|
import json
|
|
import logging
|
|
import time
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Callable, Dict, List, Optional
|
|
|
|
from rp.config import (
|
|
COMPRESSION_TRIGGER,
|
|
CONTEXT_WINDOW,
|
|
MAX_AUTONOMOUS_ITERATIONS,
|
|
STREAMING_ENABLED,
|
|
VERIFICATION_REQUIRED,
|
|
VISIBLE_REASONING,
|
|
)
|
|
from rp.core.cost_optimizer import CostOptimizer, create_cost_optimizer
|
|
from rp.core.error_handler import ErrorHandler, ErrorDetection
|
|
from rp.core.reasoning import ReasoningEngine, ReasoningTrace
|
|
from rp.core.think_tool import ThinkTool, DecisionPoint, DecisionType
|
|
from rp.core.tool_selector import ToolSelector
|
|
from rp.ui import Colors
|
|
|
|
logger = logging.getLogger("rp")
|
|
|
|
|
|
@dataclass
|
|
class ExecutionContext:
|
|
request: str
|
|
filesystem_state: Dict[str, Any] = field(default_factory=dict)
|
|
environment: Dict[str, Any] = field(default_factory=dict)
|
|
command_history: List[str] = field(default_factory=list)
|
|
cache_available: bool = False
|
|
token_budget: int = 0
|
|
iteration: int = 0
|
|
accumulated_results: List[Dict[str, Any]] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class ExecutionPlan:
|
|
intent: Dict[str, Any]
|
|
constraints: Dict[str, Any]
|
|
tools: List[str]
|
|
sequence: List[Dict[str, Any]]
|
|
success_criteria: List[str]
|
|
|
|
|
|
@dataclass
|
|
class VerificationResult:
|
|
is_complete: bool
|
|
criteria_met: Dict[str, bool]
|
|
quality_score: float
|
|
needs_retry: bool
|
|
retry_reason: Optional[str] = None
|
|
errors_found: List[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class AgentResponse:
|
|
content: str
|
|
tool_results: List[Dict[str, Any]]
|
|
verification: VerificationResult
|
|
reasoning_trace: Optional[ReasoningTrace]
|
|
cost_breakdown: Optional[Dict[str, Any]]
|
|
iterations: int
|
|
duration: float
|
|
|
|
|
|
class ContextGatherer:
|
|
def __init__(self, assistant):
|
|
self.assistant = assistant
|
|
|
|
def gather(self, request: str) -> ExecutionContext:
|
|
context = ExecutionContext(request=request)
|
|
with ThreadPoolExecutor(max_workers=4) as executor:
|
|
futures = {
|
|
executor.submit(self._get_filesystem_state): 'filesystem',
|
|
executor.submit(self._get_environment): 'environment',
|
|
executor.submit(self._get_command_history): 'history',
|
|
executor.submit(self._check_cache, request): 'cache'
|
|
}
|
|
for future in as_completed(futures):
|
|
key = futures[future]
|
|
try:
|
|
result = future.result()
|
|
if key == 'filesystem':
|
|
context.filesystem_state = result
|
|
elif key == 'environment':
|
|
context.environment = result
|
|
elif key == 'history':
|
|
context.command_history = result
|
|
elif key == 'cache':
|
|
context.cache_available = result
|
|
except Exception as e:
|
|
logger.warning(f"Context gathering failed for {key}: {e}")
|
|
context.token_budget = self._calculate_token_budget()
|
|
return context
|
|
|
|
def _get_filesystem_state(self) -> Dict[str, Any]:
|
|
import os
|
|
try:
|
|
cwd = os.getcwd()
|
|
items = os.listdir(cwd)[:20]
|
|
return {
|
|
'cwd': cwd,
|
|
'items': items,
|
|
'item_count': len(os.listdir(cwd))
|
|
}
|
|
except Exception as e:
|
|
return {'error': str(e)}
|
|
|
|
def _get_environment(self) -> Dict[str, Any]:
|
|
import os
|
|
return {
|
|
'cwd': os.getcwd(),
|
|
'user': os.environ.get('USER', 'unknown'),
|
|
'home': os.environ.get('HOME', ''),
|
|
'shell': os.environ.get('SHELL', ''),
|
|
'path_count': len(os.environ.get('PATH', '').split(':'))
|
|
}
|
|
|
|
def _get_command_history(self) -> List[str]:
|
|
if hasattr(self.assistant, 'messages'):
|
|
history = []
|
|
for msg in self.assistant.messages[-10:]:
|
|
if msg.get('role') == 'assistant':
|
|
content = msg.get('content', '')
|
|
if content and len(content) < 200:
|
|
history.append(content[:100])
|
|
return history
|
|
return []
|
|
|
|
def _check_cache(self, request: str) -> bool:
|
|
if hasattr(self.assistant, 'api_cache') and self.assistant.api_cache:
|
|
return True
|
|
return False
|
|
|
|
def _calculate_token_budget(self) -> int:
|
|
current_tokens = 0
|
|
if hasattr(self.assistant, 'messages'):
|
|
for msg in self.assistant.messages:
|
|
content = json.dumps(msg)
|
|
current_tokens += len(content) // 4
|
|
remaining = CONTEXT_WINDOW - current_tokens
|
|
return max(0, remaining)
|
|
|
|
def update(self, context: ExecutionContext, results: List[Dict[str, Any]]) -> ExecutionContext:
|
|
context.accumulated_results.extend(results)
|
|
context.iteration += 1
|
|
context.token_budget = self._calculate_token_budget()
|
|
return context
|
|
|
|
|
|
class ActionExecutor:
|
|
def __init__(self, assistant):
|
|
self.assistant = assistant
|
|
self.error_handler = ErrorHandler()
|
|
|
|
def execute(self, plan: ExecutionPlan, trace: ReasoningTrace) -> List[Dict[str, Any]]:
|
|
results = []
|
|
trace.start_execution()
|
|
for i, step in enumerate(plan.sequence):
|
|
tool_name = step.get('tool')
|
|
arguments = step.get('arguments', {})
|
|
prevention = self.error_handler.prevent(tool_name, arguments)
|
|
if prevention.blocked:
|
|
results.append({
|
|
'tool': tool_name,
|
|
'status': 'blocked',
|
|
'reason': prevention.reason,
|
|
'suggestions': prevention.suggestions
|
|
})
|
|
trace.add_tool_call(
|
|
tool_name,
|
|
arguments,
|
|
f"BLOCKED: {prevention.reason}",
|
|
0.0
|
|
)
|
|
continue
|
|
start_time = time.time()
|
|
try:
|
|
result = self._execute_tool(tool_name, arguments)
|
|
duration = time.time() - start_time
|
|
errors = self.error_handler.detect(result, tool_name)
|
|
if errors:
|
|
for error in errors:
|
|
recovery = self.error_handler.recover(
|
|
error, tool_name, arguments, self._execute_tool
|
|
)
|
|
self.error_handler.learn(error, recovery)
|
|
if recovery.success:
|
|
result = recovery.result
|
|
break
|
|
results.append({
|
|
'tool': tool_name,
|
|
'status': result.get('status', 'unknown'),
|
|
'result': result,
|
|
'duration': duration
|
|
})
|
|
trace.add_tool_call(tool_name, arguments, result, duration)
|
|
except Exception as e:
|
|
duration = time.time() - start_time
|
|
error_result = {'status': 'error', 'error': str(e)}
|
|
results.append({
|
|
'tool': tool_name,
|
|
'status': 'error',
|
|
'error': str(e),
|
|
'duration': duration
|
|
})
|
|
trace.add_tool_call(tool_name, arguments, error_result, duration)
|
|
trace.end_execution()
|
|
return results
|
|
|
|
def _execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
if hasattr(self.assistant, 'tool_executor'):
|
|
from rp.core.tool_executor import ToolCall
|
|
tool_call = ToolCall(
|
|
tool_id=f"exec_{int(time.time())}",
|
|
function_name=tool_name,
|
|
arguments=arguments
|
|
)
|
|
results = self.assistant.tool_executor.execute_sequential([tool_call])
|
|
if results:
|
|
return results[0].result if results[0].success else {'status': 'error', 'error': results[0].error}
|
|
return {'status': 'error', 'error': f'Tool not available: {tool_name}'}
|
|
|
|
|
|
class Verifier:
|
|
def __init__(self, assistant):
|
|
self.assistant = assistant
|
|
|
|
def verify(
|
|
self,
|
|
results: List[Dict[str, Any]],
|
|
request: str,
|
|
plan: ExecutionPlan,
|
|
trace: ReasoningTrace
|
|
) -> VerificationResult:
|
|
if not VERIFICATION_REQUIRED:
|
|
return VerificationResult(
|
|
is_complete=True,
|
|
criteria_met={},
|
|
quality_score=1.0,
|
|
needs_retry=False
|
|
)
|
|
trace.start_verification()
|
|
criteria_met = {}
|
|
errors_found = []
|
|
for criterion in plan.success_criteria:
|
|
passed = self._check_criterion(criterion, results)
|
|
criteria_met[criterion] = passed
|
|
trace.add_verification(criterion, passed)
|
|
if not passed:
|
|
errors_found.append(f"Criterion not met: {criterion}")
|
|
for result in results:
|
|
if result.get('status') == 'error':
|
|
errors_found.append(f"Tool error: {result.get('error', 'unknown')}")
|
|
if result.get('status') == 'blocked':
|
|
errors_found.append(f"Tool blocked: {result.get('reason', 'unknown')}")
|
|
quality_score = self._calculate_quality_score(results, criteria_met)
|
|
all_criteria_met = all(criteria_met.values()) if criteria_met else True
|
|
has_errors = len(errors_found) > 0
|
|
is_complete = all_criteria_met and not has_errors and quality_score >= 0.7
|
|
needs_retry = not is_complete and quality_score < 0.5
|
|
trace.end_verification()
|
|
return VerificationResult(
|
|
is_complete=is_complete,
|
|
criteria_met=criteria_met,
|
|
quality_score=quality_score,
|
|
needs_retry=needs_retry,
|
|
retry_reason="Quality threshold not met" if needs_retry else None,
|
|
errors_found=errors_found
|
|
)
|
|
|
|
def _check_criterion(self, criterion: str, results: List[Dict[str, Any]]) -> bool:
|
|
criterion_lower = criterion.lower()
|
|
if 'no errors' in criterion_lower or 'error-free' in criterion_lower:
|
|
return all(r.get('status') != 'error' for r in results)
|
|
if 'success' in criterion_lower:
|
|
return any(r.get('status') == 'success' for r in results)
|
|
if 'file created' in criterion_lower or 'file written' in criterion_lower:
|
|
return any(
|
|
r.get('tool') in ['write_file', 'create_file'] and r.get('status') == 'success'
|
|
for r in results
|
|
)
|
|
if 'command executed' in criterion_lower:
|
|
return any(
|
|
r.get('tool') == 'run_command' and r.get('status') == 'success'
|
|
for r in results
|
|
)
|
|
return True
|
|
|
|
def _calculate_quality_score(
|
|
self,
|
|
results: List[Dict[str, Any]],
|
|
criteria_met: Dict[str, bool]
|
|
) -> float:
|
|
if not results:
|
|
return 0.5
|
|
success_count = sum(1 for r in results if r.get('status') == 'success')
|
|
error_count = sum(1 for r in results if r.get('status') == 'error')
|
|
blocked_count = sum(1 for r in results if r.get('status') == 'blocked')
|
|
total = len(results)
|
|
execution_score = success_count / total if total > 0 else 0
|
|
criteria_score = sum(1 for v in criteria_met.values() if v) / len(criteria_met) if criteria_met else 1
|
|
error_penalty = error_count * 0.2
|
|
blocked_penalty = blocked_count * 0.1
|
|
score = (execution_score * 0.6 + criteria_score * 0.4) - error_penalty - blocked_penalty
|
|
return max(0.0, min(1.0, score))
|
|
|
|
|
|
class AgentLoop:
|
|
def __init__(self, assistant):
|
|
self.assistant = assistant
|
|
self.context_gatherer = ContextGatherer(assistant)
|
|
self.reasoning_engine = ReasoningEngine(visible=VISIBLE_REASONING)
|
|
self.tool_selector = ToolSelector()
|
|
self.action_executor = ActionExecutor(assistant)
|
|
self.verifier = Verifier(assistant)
|
|
self.think_tool = ThinkTool(visible=VISIBLE_REASONING)
|
|
self.cost_optimizer = create_cost_optimizer()
|
|
|
|
def execute(self, request: str) -> AgentResponse:
|
|
start_time = time.time()
|
|
trace = self.reasoning_engine.start_trace()
|
|
context = self.context_gatherer.gather(request)
|
|
optimizations = self.cost_optimizer.suggest_optimization(request, {
|
|
'message_count': len(self.assistant.messages) if hasattr(self.assistant, 'messages') else 0,
|
|
'has_cache_prefix': context.cache_available
|
|
})
|
|
all_results = []
|
|
iterations = 0
|
|
while iterations < MAX_AUTONOMOUS_ITERATIONS:
|
|
iterations += 1
|
|
context.iteration = iterations
|
|
trace.start_thinking()
|
|
intent = self.reasoning_engine.extract_intent(request)
|
|
trace.add_thinking(f"Intent: {intent['task_type']} (complexity: {intent['complexity']})")
|
|
if intent['is_destructive']:
|
|
trace.add_thinking("Warning: Destructive operation detected - will request confirmation")
|
|
constraints = self.reasoning_engine.analyze_constraints(request, context.__dict__)
|
|
selection = self.tool_selector.select(request, context.__dict__)
|
|
trace.add_thinking(f"Tool selection: {selection.reasoning}")
|
|
trace.add_thinking(f"Execution pattern: {selection.execution_pattern}")
|
|
trace.end_thinking()
|
|
plan = ExecutionPlan(
|
|
intent=intent,
|
|
constraints=constraints,
|
|
tools=[s.tool for s in selection.decisions],
|
|
sequence=[
|
|
{'tool': s.tool, 'arguments': s.arguments_hint}
|
|
for s in selection.decisions
|
|
],
|
|
success_criteria=self._generate_success_criteria(intent)
|
|
)
|
|
results = self.action_executor.execute(plan, trace)
|
|
all_results.extend(results)
|
|
verification = self.verifier.verify(results, request, plan, trace)
|
|
if verification.is_complete:
|
|
break
|
|
if verification.needs_retry:
|
|
trace.add_thinking(f"Retry needed: {verification.retry_reason}")
|
|
context = self.context_gatherer.update(context, results)
|
|
else:
|
|
break
|
|
duration = time.time() - start_time
|
|
content = self._generate_response_content(all_results, trace)
|
|
return AgentResponse(
|
|
content=content,
|
|
tool_results=all_results,
|
|
verification=verification,
|
|
reasoning_trace=trace,
|
|
cost_breakdown=None,
|
|
iterations=iterations,
|
|
duration=duration
|
|
)
|
|
|
|
def _generate_success_criteria(self, intent: Dict[str, Any]) -> List[str]:
|
|
criteria = ['no errors']
|
|
task_type = intent.get('task_type', 'general')
|
|
if task_type in ['create', 'modify']:
|
|
criteria.append('file operation successful')
|
|
if task_type == 'execute':
|
|
criteria.append('command executed successfully')
|
|
if task_type == 'query':
|
|
criteria.append('information retrieved')
|
|
return criteria
|
|
|
|
def _generate_response_content(
|
|
self,
|
|
results: List[Dict[str, Any]],
|
|
trace: ReasoningTrace
|
|
) -> str:
|
|
content_parts = []
|
|
successful = [r for r in results if r.get('status') == 'success']
|
|
failed = [r for r in results if r.get('status') in ['error', 'blocked']]
|
|
if successful:
|
|
for result in successful:
|
|
tool = result.get('tool', 'unknown')
|
|
output = result.get('result', {})
|
|
if isinstance(output, dict):
|
|
output_str = output.get('output', output.get('content', str(output)))
|
|
else:
|
|
output_str = str(output)
|
|
if len(output_str) > 500:
|
|
output_str = output_str[:497] + "..."
|
|
content_parts.append(f"[{tool}] {output_str}")
|
|
if failed:
|
|
content_parts.append("\nErrors encountered:")
|
|
for result in failed:
|
|
tool = result.get('tool', 'unknown')
|
|
error = result.get('error') or result.get('reason', 'unknown error')
|
|
content_parts.append(f" - {tool}: {error}")
|
|
if not content_parts:
|
|
content_parts.append("No operations performed.")
|
|
return "\n".join(content_parts)
|
|
|
|
|
|
def create_agent_loop(assistant) -> AgentLoop:
|
|
return AgentLoop(assistant)
|