|
import base64
|
|
import json
|
|
import logging
|
|
import time
|
|
|
|
from rp.autonomous.detection import is_task_complete, get_completion_reason
|
|
from rp.autonomous.verification import TaskVerifier, create_task_verifier
|
|
from rp.config import STREAMING_ENABLED, VISIBLE_REASONING, VERIFICATION_REQUIRED
|
|
from rp.core.api import call_api
|
|
from rp.core.context import truncate_tool_result
|
|
from rp.core.cost_optimizer import CostOptimizer, create_cost_optimizer
|
|
from rp.core.debug import debug_trace
|
|
from rp.core.error_handler import ErrorHandler
|
|
from rp.core.reasoning import ReasoningEngine, ReasoningTrace
|
|
from rp.core.tool_selector import ToolSelector
|
|
from rp.tools.base import get_tools_definition
|
|
from rp.ui import Colors
|
|
from rp.ui.progress import ProgressIndicator
|
|
|
|
logger = logging.getLogger("rp")
|
|
|
|
|
|
def extract_reasoning_and_clean_content(content):
|
|
reasoning = None
|
|
lines = content.split("\n")
|
|
cleaned_lines = []
|
|
for line in lines:
|
|
if line.strip().startswith("REASONING:"):
|
|
reasoning = line.strip()[10:].strip()
|
|
else:
|
|
cleaned_lines.append(line)
|
|
cleaned_content = "\n".join(cleaned_lines)
|
|
cleaned_content = cleaned_content.replace("[TASK_COMPLETE]", "").strip()
|
|
return reasoning, cleaned_content
|
|
|
|
|
|
def sanitize_for_json(obj):
|
|
if isinstance(obj, bytes):
|
|
return base64.b64encode(obj).decode("utf-8")
|
|
elif isinstance(obj, dict):
|
|
return {k: sanitize_for_json(v) for k, v in obj.items()}
|
|
elif isinstance(obj, (list, tuple)):
|
|
return [sanitize_for_json(item) for item in obj]
|
|
else:
|
|
return obj
|
|
|
|
|
|
class AutonomousExecutor:
|
|
def __init__(self, assistant):
|
|
self.assistant = assistant
|
|
self.visible_reasoning = bool(assistant.verbose)
|
|
self.reasoning_engine = ReasoningEngine(visible=self.visible_reasoning)
|
|
self.tool_selector = ToolSelector()
|
|
self.error_handler = ErrorHandler()
|
|
self.cost_optimizer = create_cost_optimizer()
|
|
self.verifier = create_task_verifier(visible=self.visible_reasoning)
|
|
self.current_trace = None
|
|
self.tool_results = []
|
|
|
|
@debug_trace
|
|
def execute(self, task: str):
|
|
self.assistant.autonomous_mode = True
|
|
self.assistant.autonomous_iterations = 0
|
|
last_printed_result = None
|
|
logger.debug("=== AUTONOMOUS MODE START ===")
|
|
logger.debug(f"Task: {task}")
|
|
|
|
print(f"\n{Colors.BOLD}{Colors.CYAN}{'═' * 70}{Colors.RESET}")
|
|
print(f"{Colors.BOLD}Task:{Colors.RESET} {task[:100]}{'...' if len(task) > 100 else ''}")
|
|
print(f"{Colors.GRAY}Working autonomously. Press Ctrl+C to interrupt.{Colors.RESET}")
|
|
print(f"{Colors.BOLD}{Colors.CYAN}{'═' * 70}{Colors.RESET}\n")
|
|
|
|
self.current_trace = self.reasoning_engine.start_trace()
|
|
from rp.core.knowledge_context import inject_knowledge_context
|
|
|
|
self.current_trace.start_thinking()
|
|
intent = self.reasoning_engine.extract_intent(task)
|
|
self.current_trace.add_thinking(f"Task type: {intent['task_type']}, Complexity: {intent['complexity']}")
|
|
|
|
if intent['is_destructive']:
|
|
self.current_trace.add_thinking("Destructive operation detected - will proceed with caution")
|
|
|
|
if intent['requires_tools']:
|
|
selection = self.tool_selector.select(task, {'request': task})
|
|
self.current_trace.add_thinking(f"Tool strategy: {selection.reasoning}")
|
|
self.current_trace.add_thinking(f"Execution pattern: {selection.execution_pattern}")
|
|
self.current_trace.end_thinking()
|
|
|
|
optimizations = self.cost_optimizer.suggest_optimization(task, {
|
|
'message_count': len(self.assistant.messages),
|
|
'has_cache_prefix': hasattr(self.assistant, 'api_cache') and self.assistant.api_cache is not None
|
|
})
|
|
if optimizations and VISIBLE_REASONING:
|
|
for opt in optimizations:
|
|
if opt.estimated_savings > 0:
|
|
logger.debug(f"Optimization available: {opt.strategy.value} ({opt.estimated_savings:.0%} savings)")
|
|
|
|
self.assistant.messages.append({"role": "user", "content": f"{task}"})
|
|
|
|
if hasattr(self.assistant, "memory_manager"):
|
|
self.assistant.memory_manager.process_message(
|
|
task, role="user", extract_facts=True, update_graph=True
|
|
)
|
|
logger.debug("Extracted facts from user task and stored in memory")
|
|
|
|
inject_knowledge_context(self.assistant, self.assistant.messages[-1]["content"])
|
|
|
|
try:
|
|
while True:
|
|
self.assistant.autonomous_iterations += 1
|
|
iteration = self.assistant.autonomous_iterations
|
|
logger.debug(f"--- Autonomous iteration {iteration} ---")
|
|
logger.debug(f"Messages before context management: {len(self.assistant.messages)}")
|
|
|
|
if iteration > 1:
|
|
print(f"{Colors.GRAY}─── Iteration {iteration} ───{Colors.RESET}")
|
|
|
|
from rp.core.context import manage_context_window, refresh_system_message
|
|
|
|
self.assistant.messages = manage_context_window(self.assistant.messages, self.assistant.verbose)
|
|
logger.debug(f"Messages after context management: {len(self.assistant.messages)}")
|
|
|
|
with ProgressIndicator("Querying AI..."):
|
|
refresh_system_message(self.assistant.messages, self.assistant.args)
|
|
response = call_api(
|
|
self.assistant.messages,
|
|
self.assistant.model,
|
|
self.assistant.api_url,
|
|
self.assistant.api_key,
|
|
self.assistant.use_tools,
|
|
get_tools_definition(),
|
|
verbose=self.assistant.verbose,
|
|
)
|
|
|
|
if "usage" in response:
|
|
usage = response["usage"]
|
|
input_tokens = usage.get("prompt_tokens", 0)
|
|
output_tokens = usage.get("completion_tokens", 0)
|
|
cached_tokens = usage.get("cached_tokens", 0)
|
|
cost_breakdown = self.cost_optimizer.calculate_cost(input_tokens, output_tokens, cached_tokens)
|
|
self.assistant.usage_tracker.track_request(self.assistant.model, input_tokens, output_tokens)
|
|
print(f"{Colors.YELLOW}💰 Cost: {self.cost_optimizer.format_cost(cost_breakdown.total_cost)} | "
|
|
f"Session: {self.cost_optimizer.format_cost(sum(c.total_cost for c in self.cost_optimizer.session_costs))}{Colors.RESET}")
|
|
|
|
if "error" in response:
|
|
logger.error(f"API error in autonomous mode: {response['error']}")
|
|
print(f"{Colors.RED}Error: {response['error']}{Colors.RESET}")
|
|
break
|
|
|
|
is_complete = is_task_complete(response, iteration)
|
|
|
|
if VERIFICATION_REQUIRED and is_complete:
|
|
verification = self.verifier.verify_task(
|
|
response, self.tool_results, task, iteration
|
|
)
|
|
if verification.needs_retry and iteration < 5:
|
|
is_complete = False
|
|
logger.info(f"Verification failed, retrying: {verification.retry_reason}")
|
|
|
|
logger.debug(f"Task completion check: {is_complete}")
|
|
|
|
if is_complete:
|
|
result = self._process_response(response)
|
|
if result != last_printed_result:
|
|
completion_reason = get_completion_reason(response, iteration)
|
|
if completion_reason and self.visible_reasoning:
|
|
print(f"{Colors.CYAN}[Completion: {completion_reason}]{Colors.RESET}")
|
|
print(f"\n{Colors.GREEN}r:{Colors.RESET} {result}\n")
|
|
last_printed_result = result
|
|
|
|
self._display_session_summary()
|
|
logger.debug(f"=== AUTONOMOUS MODE COMPLETE ===")
|
|
logger.debug(f"Total iterations: {iteration}")
|
|
logger.debug(f"Final message count: {len(self.assistant.messages)}")
|
|
break
|
|
|
|
result = self._process_response(response)
|
|
if result and result != last_printed_result:
|
|
print(f"\n{Colors.GREEN}r:{Colors.RESET} {result}\n")
|
|
last_printed_result = result
|
|
time.sleep(0.5)
|
|
|
|
except KeyboardInterrupt:
|
|
logger.debug("Autonomous mode interrupted by user")
|
|
print(f"\n{Colors.YELLOW}Autonomous mode interrupted by user{Colors.RESET}")
|
|
if self.assistant.messages and self.assistant.messages[-1]["role"] == "user":
|
|
self.assistant.messages.pop()
|
|
finally:
|
|
self.assistant.autonomous_mode = False
|
|
logger.debug("=== AUTONOMOUS MODE END ===")
|
|
|
|
def _process_response(self, response):
|
|
if "error" in response:
|
|
return f"Error: {response['error']}"
|
|
if "choices" not in response or not response["choices"]:
|
|
return "No response from API"
|
|
|
|
message = response["choices"][0]["message"]
|
|
self.assistant.messages.append(message)
|
|
|
|
if "tool_calls" in message and message["tool_calls"]:
|
|
self.current_trace.start_execution()
|
|
tool_results = []
|
|
|
|
for tool_call in message["tool_calls"]:
|
|
func_name = tool_call["function"]["name"]
|
|
arguments = json.loads(tool_call["function"]["arguments"])
|
|
|
|
prevention = self.error_handler.prevent(func_name, arguments)
|
|
if prevention.blocked:
|
|
print(f"{Colors.RED}⚠ Blocked: {func_name} - {prevention.reason}{Colors.RESET}")
|
|
self.current_trace.add_tool_call(func_name, arguments, f"BLOCKED: {prevention.reason}", 0)
|
|
tool_results.append({
|
|
"tool_call_id": tool_call["id"],
|
|
"role": "tool",
|
|
"content": json.dumps({"status": "error", "error": f"Blocked: {prevention.reason}"})
|
|
})
|
|
continue
|
|
|
|
args_str = ", ".join([f"{k}={repr(v)[:50]}" for k, v in arguments.items()])
|
|
if len(args_str) > 80:
|
|
args_str = args_str[:77] + "..."
|
|
print(f"{Colors.BLUE}▶ {func_name}({args_str}){Colors.RESET}", end="", flush=True)
|
|
|
|
start_time = time.time()
|
|
result = execute_single_tool(self.assistant, func_name, arguments)
|
|
duration = time.time() - start_time
|
|
|
|
if isinstance(result, str):
|
|
try:
|
|
result = json.loads(result)
|
|
except json.JSONDecodeError as ex:
|
|
result = {"error": str(ex)}
|
|
|
|
is_error = isinstance(result, dict) and (result.get("status") == "error" or "error" in result)
|
|
if is_error:
|
|
print(f" {Colors.RED}✗ ({duration:.1f}s){Colors.RESET}")
|
|
error_msg = result.get("error", "Unknown error")[:100]
|
|
print(f" {Colors.RED}└─ {error_msg}{Colors.RESET}")
|
|
else:
|
|
print(f" {Colors.GREEN}✓ ({duration:.1f}s){Colors.RESET}")
|
|
|
|
errors = self.error_handler.detect(result, func_name)
|
|
if errors:
|
|
for error in errors:
|
|
recovery = self.error_handler.recover(
|
|
error, func_name, arguments,
|
|
lambda name, args: execute_single_tool(self.assistant, name, args)
|
|
)
|
|
self.error_handler.learn(error, recovery)
|
|
if recovery.success:
|
|
result = recovery.result
|
|
print(f" {Colors.GREEN}└─ Recovered: {recovery.message}{Colors.RESET}")
|
|
break
|
|
elif recovery.needs_human:
|
|
print(f" {Colors.YELLOW}└─ {recovery.error}{Colors.RESET}")
|
|
|
|
self.current_trace.add_tool_call(func_name, arguments, result, duration)
|
|
|
|
result = truncate_tool_result(result)
|
|
sanitized_result = sanitize_for_json(result)
|
|
tool_results.append({
|
|
"tool_call_id": tool_call["id"],
|
|
"role": "tool",
|
|
"content": json.dumps(sanitized_result),
|
|
})
|
|
|
|
self.tool_results.append({
|
|
'tool': func_name,
|
|
'status': result.get('status', 'unknown') if isinstance(result, dict) else 'success',
|
|
'result': result,
|
|
'duration': duration
|
|
})
|
|
|
|
self.current_trace.end_execution()
|
|
|
|
for result in tool_results:
|
|
self.assistant.messages.append(result)
|
|
|
|
with ProgressIndicator("Processing tool results..."):
|
|
from rp.core.context import refresh_system_message
|
|
|
|
refresh_system_message(self.assistant.messages, self.assistant.args)
|
|
follow_up = call_api(
|
|
self.assistant.messages,
|
|
self.assistant.model,
|
|
self.assistant.api_url,
|
|
self.assistant.api_key,
|
|
self.assistant.use_tools,
|
|
get_tools_definition(),
|
|
verbose=self.assistant.verbose,
|
|
)
|
|
|
|
if "usage" in follow_up:
|
|
usage = follow_up["usage"]
|
|
input_tokens = usage.get("prompt_tokens", 0)
|
|
output_tokens = usage.get("completion_tokens", 0)
|
|
cached_tokens = usage.get("cached_tokens", 0)
|
|
self.cost_optimizer.calculate_cost(input_tokens, output_tokens, cached_tokens)
|
|
self.assistant.usage_tracker.track_request(self.assistant.model, input_tokens, output_tokens)
|
|
|
|
return self._process_response(follow_up)
|
|
|
|
content = message.get("content", "")
|
|
reasoning, cleaned_content = extract_reasoning_and_clean_content(content)
|
|
|
|
if reasoning and VISIBLE_REASONING:
|
|
print(f"{Colors.BLUE}💭 Reasoning: {reasoning}{Colors.RESET}")
|
|
|
|
from rp.ui import render_markdown
|
|
return render_markdown(cleaned_content, self.assistant.syntax_highlighting)
|
|
|
|
def _display_session_summary(self):
|
|
if not self.visible_reasoning:
|
|
return
|
|
|
|
summary = self.cost_optimizer.get_session_summary()
|
|
if summary.total_requests > 0:
|
|
print(f"\n{Colors.CYAN}━━━ Session Summary ━━━{Colors.RESET}")
|
|
print(f" Requests: {summary.total_requests}")
|
|
print(f" Total tokens: {summary.total_input_tokens + summary.total_output_tokens}")
|
|
print(f" Total cost: {self.cost_optimizer.format_cost(summary.total_cost)}")
|
|
if summary.total_savings > 0:
|
|
print(f" Savings: {self.cost_optimizer.format_cost(summary.total_savings)}")
|
|
|
|
error_stats = self.error_handler.get_statistics()
|
|
if error_stats.get('total_errors', 0) > 0:
|
|
print(f" Errors handled: {error_stats['total_errors']}")
|
|
|
|
trace_summary = self.current_trace.get_summary() if self.current_trace else {}
|
|
if trace_summary.get('execution_steps', 0) > 0:
|
|
print(f" Tool calls: {trace_summary['execution_steps']}")
|
|
print(f" Duration: {trace_summary['total_duration']:.1f}s")
|
|
|
|
print(f"{Colors.CYAN}━━━━━━━━━━━━━━━━━━━━━━━{Colors.RESET}\n")
|
|
|
|
|
|
def run_autonomous_mode(assistant, task):
|
|
executor = AutonomousExecutor(assistant)
|
|
executor.execute(task)
|
|
|
|
|
|
def process_response_autonomous(assistant, response):
|
|
if "error" in response:
|
|
return f"Error: {response['error']}"
|
|
if "choices" not in response or not response["choices"]:
|
|
return "No response from API"
|
|
message = response["choices"][0]["message"]
|
|
assistant.messages.append(message)
|
|
if "tool_calls" in message and message["tool_calls"]:
|
|
tool_results = []
|
|
for tool_call in message["tool_calls"]:
|
|
func_name = tool_call["function"]["name"]
|
|
arguments = json.loads(tool_call["function"]["arguments"])
|
|
args_str = ", ".join([f"{k}={repr(v)[:50]}" for k, v in arguments.items()])
|
|
if len(args_str) > 80:
|
|
args_str = args_str[:77] + "..."
|
|
print(f"{Colors.BLUE}▶ {func_name}({args_str}){Colors.RESET}", end="", flush=True)
|
|
start_time = time.time()
|
|
result = execute_single_tool(assistant, func_name, arguments)
|
|
duration = time.time() - start_time
|
|
if isinstance(result, str):
|
|
try:
|
|
result = json.loads(result)
|
|
except json.JSONDecodeError as ex:
|
|
result = {"error": str(ex)}
|
|
is_error = isinstance(result, dict) and (result.get("status") == "error" or "error" in result)
|
|
if is_error:
|
|
print(f" {Colors.RED}✗ ({duration:.1f}s){Colors.RESET}")
|
|
error_msg = result.get("error", "Unknown error")[:100]
|
|
print(f" {Colors.RED}└─ {error_msg}{Colors.RESET}")
|
|
else:
|
|
print(f" {Colors.GREEN}✓ ({duration:.1f}s){Colors.RESET}")
|
|
result = truncate_tool_result(result)
|
|
sanitized_result = sanitize_for_json(result)
|
|
tool_results.append({
|
|
"tool_call_id": tool_call["id"],
|
|
"role": "tool",
|
|
"content": json.dumps(sanitized_result),
|
|
})
|
|
for result in tool_results:
|
|
assistant.messages.append(result)
|
|
with ProgressIndicator("Processing tool results..."):
|
|
from rp.core.context import refresh_system_message
|
|
refresh_system_message(assistant.messages, assistant.args)
|
|
follow_up = call_api(
|
|
assistant.messages,
|
|
assistant.model,
|
|
assistant.api_url,
|
|
assistant.api_key,
|
|
assistant.use_tools,
|
|
get_tools_definition(),
|
|
verbose=assistant.verbose,
|
|
)
|
|
if "usage" in follow_up:
|
|
usage = follow_up["usage"]
|
|
input_tokens = usage.get("prompt_tokens", 0)
|
|
output_tokens = usage.get("completion_tokens", 0)
|
|
assistant.usage_tracker.track_request(assistant.model, input_tokens, output_tokens)
|
|
cost = assistant.usage_tracker._calculate_cost(assistant.model, input_tokens, output_tokens)
|
|
total_cost = assistant.usage_tracker.session_usage["estimated_cost"]
|
|
print(f"{Colors.YELLOW}Cost: ${cost:.4f} | Total: ${total_cost:.4f}{Colors.RESET}")
|
|
return process_response_autonomous(assistant, follow_up)
|
|
content = message.get("content", "")
|
|
reasoning, cleaned_content = extract_reasoning_and_clean_content(content)
|
|
if reasoning:
|
|
print(f"{Colors.BLUE}Reasoning: {reasoning}{Colors.RESET}")
|
|
from rp.ui import render_markdown
|
|
return render_markdown(cleaned_content, assistant.syntax_highlighting)
|
|
|
|
|
|
def execute_single_tool(assistant, func_name, arguments):
|
|
logger.debug(f"Executing tool in autonomous mode: {func_name}")
|
|
logger.debug(f"Tool arguments: {arguments}")
|
|
from rp.tools.base import get_func_map
|
|
func_map = get_func_map(db_conn=assistant.db_conn, python_globals=assistant.python_globals)
|
|
if func_name in func_map:
|
|
try:
|
|
result = func_map[func_name](**arguments)
|
|
logger.debug(f"Tool execution result: {str(result)[:200]}...")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Tool execution error: {str(e)}")
|
|
return {"status": "error", "error": str(e)}
|
|
else:
|
|
logger.error(f"Unknown function requested: {func_name}")
|
|
return {"status": "error", "error": f"Unknown function: {func_name}"}
|