import os import json import logging from pr.config import (CONTEXT_FILE, GLOBAL_CONTEXT_FILE, CONTEXT_COMPRESSION_THRESHOLD, RECENT_MESSAGES_TO_KEEP, MAX_TOKENS_LIMIT, CHARS_PER_TOKEN, EMERGENCY_MESSAGES_TO_KEEP, CONTENT_TRIM_LENGTH, MAX_TOOL_RESULT_LENGTH) from pr.ui import Colors def truncate_tool_result(result, max_length=None): if max_length is None: max_length = MAX_TOOL_RESULT_LENGTH if not isinstance(result, dict): return result result_copy = result.copy() if "output" in result_copy and isinstance(result_copy["output"], str): if len(result_copy["output"]) > max_length: result_copy["output"] = result_copy["output"][:max_length] + f"\n... [truncated {len(result_copy['output']) - max_length} chars]" if "content" in result_copy and isinstance(result_copy["content"], str): if len(result_copy["content"]) > max_length: result_copy["content"] = result_copy["content"][:max_length] + f"\n... [truncated {len(result_copy['content']) - max_length} chars]" if "data" in result_copy and isinstance(result_copy["data"], str): if len(result_copy["data"]) > max_length: result_copy["data"] = result_copy["data"][:max_length] + f"\n... [truncated]" if "error" in result_copy and isinstance(result_copy["error"], str): if len(result_copy["error"]) > max_length // 2: result_copy["error"] = result_copy["error"][:max_length // 2] + "... [truncated]" return result_copy def init_system_message(args): context_parts = ["""You are a professional AI assistant with access to advanced tools. File Operations: - Use RPEditor tools (open_editor, editor_insert_text, editor_replace_text, editor_search, close_editor) for precise file modifications - Always close editor files when finished - Use write_file for complete file rewrites, search_replace for simple text replacements Process Management: - run_command executes shell commands with a timeout (default 30s) - If a command times out, you receive a PID in the response - Use tail_process(pid) to monitor running processes - Use kill_process(pid) to terminate processes - Manage long-running commands effectively using these tools Shell Commands: - Be a shell ninja using native OS tools - Prefer standard Unix utilities over complex scripts - Use run_command_interactive for commands requiring user input (vim, nano, etc.)"""] #context_parts = ["You are a helpful AI assistant with access to advanced tools, including a powerful built-in editor (RPEditor). For file editing tasks, prefer using the editor-related tools like write_file, search_replace, open_editor, editor_insert_text, editor_replace_text, and editor_search, as they provide advanced editing capabilities with undo/redo, search, and precise text manipulation. The editor is integrated seamlessly and should be your primary tool for modifying files."] max_context_size = 10000 if args.include_env: env_context = "Environment Variables:\n" for key, value in os.environ.items(): if not key.startswith('_'): env_context += f"{key}={value}\n" if len(env_context) > max_context_size: env_context = env_context[:max_context_size] + "\n... [truncated]" context_parts.append(env_context) for context_file in [CONTEXT_FILE, GLOBAL_CONTEXT_FILE]: if os.path.exists(context_file): try: with open(context_file, 'r') as f: content = f.read() if len(content) > max_context_size: content = content[:max_context_size] + "\n... [truncated]" context_parts.append(f"Context from {context_file}:\n{content}") except Exception as e: logging.error(f"Error reading context file {context_file}: {e}") if args.context: for ctx_file in args.context: try: with open(ctx_file, 'r') as f: content = f.read() if len(content) > max_context_size: content = content[:max_context_size] + "\n... [truncated]" context_parts.append(f"Context from {ctx_file}:\n{content}") except Exception as e: logging.error(f"Error reading context file {ctx_file}: {e}") system_message = "\n\n".join(context_parts) if len(system_message) > max_context_size * 3: system_message = system_message[:max_context_size * 3] + "\n... [system message truncated]" return {"role": "system", "content": system_message} def should_compress_context(messages): return len(messages) > CONTEXT_COMPRESSION_THRESHOLD def compress_context(messages): return manage_context_window(messages, verbose=False) def manage_context_window(messages, verbose): if len(messages) <= CONTEXT_COMPRESSION_THRESHOLD: return messages if verbose: print(f"{Colors.YELLOW}📄 Managing context window (current: {len(messages)} messages)...{Colors.RESET}") system_message = messages[0] recent_messages = messages[-RECENT_MESSAGES_TO_KEEP:] middle_messages = messages[1:-RECENT_MESSAGES_TO_KEEP] if middle_messages: summary = summarize_messages(middle_messages) summary_message = { "role": "system", "content": f"[Previous conversation summary: {summary}]" } new_messages = [system_message, summary_message] + recent_messages if verbose: print(f"{Colors.GREEN}✓ Context compressed to {len(new_messages)} messages{Colors.RESET}") return new_messages return messages def summarize_messages(messages): summary_parts = [] for msg in messages: role = msg.get("role", "unknown") content = msg.get("content", "") if role == "tool": continue if isinstance(content, str) and len(content) > 200: content = content[:200] + "..." summary_parts.append(f"{role}: {content}") return " | ".join(summary_parts[:10]) def estimate_tokens(messages): total_chars = 0 for msg in messages: msg_json = json.dumps(msg) total_chars += len(msg_json) estimated_tokens = total_chars / CHARS_PER_TOKEN overhead_multiplier = 1.3 return int(estimated_tokens * overhead_multiplier) def trim_message_content(message, max_length): trimmed_msg = message.copy() if "content" in trimmed_msg: content = trimmed_msg["content"] if isinstance(content, str) and len(content) > max_length: trimmed_msg["content"] = content[:max_length] + f"\n... [trimmed {len(content) - max_length} chars]" elif isinstance(content, list): trimmed_content = [] for item in content: if isinstance(item, dict): trimmed_item = item.copy() if "text" in trimmed_item and len(trimmed_item["text"]) > max_length: trimmed_item["text"] = trimmed_item["text"][:max_length] + f"\n... [trimmed]" trimmed_content.append(trimmed_item) else: trimmed_content.append(item) trimmed_msg["content"] = trimmed_content if trimmed_msg.get("role") == "tool": if "content" in trimmed_msg and isinstance(trimmed_msg["content"], str): content = trimmed_msg["content"] if len(content) > MAX_TOOL_RESULT_LENGTH: trimmed_msg["content"] = content[:MAX_TOOL_RESULT_LENGTH] + f"\n... [trimmed {len(content) - MAX_TOOL_RESULT_LENGTH} chars]" try: parsed = json.loads(content) if isinstance(parsed, dict): if "output" in parsed and isinstance(parsed["output"], str) and len(parsed["output"]) > MAX_TOOL_RESULT_LENGTH // 2: parsed["output"] = parsed["output"][:MAX_TOOL_RESULT_LENGTH // 2] + f"\n... [truncated]" if "content" in parsed and isinstance(parsed["content"], str) and len(parsed["content"]) > MAX_TOOL_RESULT_LENGTH // 2: parsed["content"] = parsed["content"][:MAX_TOOL_RESULT_LENGTH // 2] + f"\n... [truncated]" trimmed_msg["content"] = json.dumps(parsed) except: pass return trimmed_msg def intelligently_trim_messages(messages, target_tokens, keep_recent=3): if estimate_tokens(messages) <= target_tokens: return messages system_msg = messages[0] if messages and messages[0].get("role") == "system" else None start_idx = 1 if system_msg else 0 recent_messages = messages[-keep_recent:] if len(messages) > keep_recent else messages[start_idx:] middle_messages = messages[start_idx:-keep_recent] if len(messages) > keep_recent else [] trimmed_middle = [] for msg in middle_messages: if msg.get("role") == "tool": trimmed_middle.append(trim_message_content(msg, MAX_TOOL_RESULT_LENGTH // 2)) elif msg.get("role") in ["user", "assistant"]: trimmed_middle.append(trim_message_content(msg, CONTENT_TRIM_LENGTH)) else: trimmed_middle.append(msg) result = ([system_msg] if system_msg else []) + trimmed_middle + recent_messages if estimate_tokens(result) <= target_tokens: return result step_size = len(trimmed_middle) // 4 if len(trimmed_middle) >= 4 else 1 while len(trimmed_middle) > 0 and estimate_tokens(result) > target_tokens: remove_count = min(step_size, len(trimmed_middle)) trimmed_middle = trimmed_middle[remove_count:] result = ([system_msg] if system_msg else []) + trimmed_middle + recent_messages if estimate_tokens(result) <= target_tokens: return result keep_recent -= 1 if keep_recent > 0: return intelligently_trim_messages(messages, target_tokens, keep_recent) return ([system_msg] if system_msg else []) + messages[-1:] def auto_slim_messages(messages, verbose=False): estimated_tokens = estimate_tokens(messages) if estimated_tokens <= MAX_TOKENS_LIMIT: return messages if verbose: print(f"{Colors.YELLOW}⚠️ Token limit approaching: ~{estimated_tokens} tokens (limit: {MAX_TOKENS_LIMIT}){Colors.RESET}") print(f"{Colors.YELLOW}🔧 Intelligently trimming message content...{Colors.RESET}") result = intelligently_trim_messages(messages, MAX_TOKENS_LIMIT, keep_recent=EMERGENCY_MESSAGES_TO_KEEP) final_tokens = estimate_tokens(result) if final_tokens > MAX_TOKENS_LIMIT: if verbose: print(f"{Colors.RED}⚠️ Still over limit after trimming, applying emergency reduction...{Colors.RESET}") result = emergency_reduce_messages(result, MAX_TOKENS_LIMIT, verbose) final_tokens = estimate_tokens(result) if verbose: removed_count = len(messages) - len(result) print(f"{Colors.GREEN}✓ Optimized from {len(messages)} to {len(result)} messages{Colors.RESET}") print(f"{Colors.GREEN} Token estimate: {estimated_tokens} → {final_tokens} (~{estimated_tokens - final_tokens} saved){Colors.RESET}") if removed_count > 0: print(f"{Colors.GREEN} Removed {removed_count} older messages{Colors.RESET}") return result def emergency_reduce_messages(messages, target_tokens, verbose=False): system_msg = messages[0] if messages and messages[0].get("role") == "system" else None start_idx = 1 if system_msg else 0 keep_count = 2 while estimate_tokens(messages) > target_tokens and keep_count >= 1: if len(messages[start_idx:]) <= keep_count: break result = ([system_msg] if system_msg else []) + messages[-keep_count:] for i in range(len(result)): result[i] = trim_message_content(result[i], CONTENT_TRIM_LENGTH // 2) if estimate_tokens(result) <= target_tokens: return result keep_count -= 1 final = ([system_msg] if system_msg else []) + messages[-1:] for i in range(len(final)): if final[i].get("role") != "system": final[i] = trim_message_content(final[i], 100) return final