diff --git a/ra_aid/__main__.py b/ra_aid/__main__.py index 51f8cb9..1d1ecff 100644 --- a/ra_aid/__main__.py +++ b/ra_aid/__main__.py @@ -4,6 +4,8 @@ import glob import os import sys import shutil +from rich.panel import Panel +from rich.console import Console from langchain_anthropic import ChatAnthropic from langchain_core.messages import HumanMessage from langgraph.checkpoint.memory import MemorySaver @@ -16,6 +18,7 @@ from ra_aid.tools import ( request_implementation, read_file_tool, emit_research_subtask, fuzzy_find_project_files, ripgrep_search, list_directory_tree ) +from ra_aid.tools.note_tech_debt import BORDER_STYLE, TECH_DEBT_NOTE_EMOJI from ra_aid.tools.memory import _global_memory from ra_aid import print_agent_output, print_stage_header, print_task_header from ra_aid.tools.programmer import related_files @@ -40,7 +43,6 @@ Examples: parser.add_argument( '-m', '--message', type=str, - required=True, help='The task or query to be executed by the agent' ) parser.add_argument( @@ -53,8 +55,21 @@ Examples: action='store_true', help='Skip interactive approval for shell commands' ) + parser.add_argument( + '--review-tech-debt', + action='store_true', + help='Review existing technical debt notes' + ) + parser.add_argument( + '--clear-tech-debt', + action='store_true', + help='Clear all technical debt notes' + ) return parser.parse_args() +# Create console instance +console = Console() + # Create the base model model = ChatAnthropic(model_name="claude-3-5-sonnet-20241022") @@ -217,6 +232,23 @@ def run_research_subtasks(base_task: str, config: dict): print(f"Encountered Anthropic Internal Server Error: {e}. Retrying...") continue +def check_tech_debt_notes() -> bool: + """Check if any tech debt notes exist. + + Returns: + bool: True if tech debt notes exist, False otherwise + """ + tech_debt_dir = '.ra-aid/tech-debt' + tech_debt_files = glob.glob(os.path.join(tech_debt_dir, '*.md')) + return len(tech_debt_files) > 0 + +def clear_tech_debt_notes() -> None: + """Clear all technical debt notes.""" + tech_debt_dir = '.ra-aid/tech-debt' + if os.path.exists(tech_debt_dir): + shutil.rmtree(tech_debt_dir) + os.makedirs(tech_debt_dir) # Recreate empty directory + def validate_environment(): """Validate required environment variables and dependencies.""" missing = [] @@ -237,12 +269,17 @@ def validate_environment(): print(f"- {error}", file=sys.stderr) sys.exit(1) -def review_tech_debt(model) -> None: +def review_tech_debt() -> None: """Review any technical debt notes collected during execution.""" tech_debt_dir = '.ra-aid/tech-debt' tech_debt_files = glob.glob(os.path.join(tech_debt_dir, '*.md')) if not tech_debt_files: + console.print(Panel( + "[bold]No technical debt notes found.[/]", + border_style=BORDER_STYLE, + title=f"{TECH_DEBT_NOTE_EMOJI} Tech Debt" + )) return print_stage_header("Technical Debt Review") @@ -252,8 +289,26 @@ def review_tech_debt(model) -> None: for file_path in tech_debt_files: with open(file_path, 'r') as file: content = file.read() + tech_debt_contents.append("\n") tech_debt_contents.append(content) + # Create dedicated memory and agent for tech debt review + tech_debt_memory = MemorySaver() + + # Define tools for tech debt review agent - minimal set needed for analysis + # tech_debt_tools = [ + # emit_expert_context, ask_expert, read_file_tool, + # list_directory_tree, fuzzy_find_project_files, ripgrep_search, + # ] + tech_debt_tools = [] + + # Create fresh agent for tech debt review + tech_debt_agent = create_react_agent( + model, + tech_debt_tools, + checkpointer=tech_debt_memory + ) + # Analyze the tech debt notes prompt = f"""Review the following technical debt notes collected during program execution: @@ -264,102 +319,146 @@ Please provide a brief, focused analysis: 2. Highlight high-impact items 3. Suggest a rough priority order Keep the response concise and actionable. + +Remember that the user doesn't know the note ids. You'll have to reiterate the key information of the issues in whole. + +We want to prioritize items that are the highest impact relative to the level of effort required to fix them. """ - # Stream the analysis + # Stream and print the analysis while True: try: - for chunk in model.stream( + for chunk in tech_debt_agent.stream( {"messages": [HumanMessage(content=prompt)]}, {"configurable": {"thread_id": "tech-debt"}, "recursion_limit": 100} ): - print_agent_output(chunk) + print_agent_output(chunk) break except ChatAnthropic.InternalServerError as e: print(f"Encountered Anthropic Internal Server Error: {e}. Retrying...") continue + # Exit immediately after tech debt review + sys.exit(0) + def main(): """Main entry point for the ra-aid command line tool.""" try: - validate_environment() - args = parse_arguments() - base_task = args.message - config = { - "configurable": { - "thread_id": "abc123" - }, - "recursion_limit": 100, - "research_only": args.research_only, - "cowboy_mode": args.cowboy_mode - } - - # Store config in global memory for access by is_informational_query - _global_memory['config'] = config + try: + validate_environment() + args = parse_arguments() - # Run research stage - print_stage_header("Research Stage") - research_prompt = f"""User query: {base_task} --keep it simple + # Validate message is provided when needed + if not (args.message or args.review_tech_debt or args.clear_tech_debt): + print("Error: --message is required unless reviewing or clearing tech debt", file=sys.stderr) + sys.exit(1) + + # Handle clear tech debt request early + if args.clear_tech_debt: + clear_tech_debt_notes() + console.print(Panel( + "[bold]Technical debt notes cleared.[/]", + border_style="bright_blue", + title="๐Ÿ“ Tech Debt" + )) + return + + # Handle tech debt review request + if args.review_tech_debt: + if check_tech_debt_notes(): + review_tech_debt() + else: + console.print(Panel( + "[bold]No technical debt notes found.[/]", + border_style="bright_blue", + title="๐Ÿ“ Tech Debt" + )) + return + base_task = args.message + config = { + "configurable": { + "thread_id": "abc123" + }, + "recursion_limit": 100, + "research_only": args.research_only, + "cowboy_mode": args.cowboy_mode + } + + # Store config in global memory for access by is_informational_query + _global_memory['config'] = config + + # Run research stage + print_stage_header("Research Stage") + research_prompt = f"""User query: {base_task} --keep it simple {RESEARCH_PROMPT} Be very thorough in your research and emit lots of snippets, key facts. If you take more than a few steps, be eager to emit research subtasks.{'' if args.research_only else ' Only request implementation if the user explicitly asked for changes to be made.'}""" - try: - while True: - try: - for chunk in research_agent.stream( - {"messages": [HumanMessage(content=research_prompt)]}, - config - ): - print_agent_output(chunk) - break - except ChatAnthropic.InternalServerError as e: - print(f"Encountered Anthropic Internal Server Error: {e}. Retrying...") - continue - except TaskCompletedException as e: - print_stage_header("Task Completed") - raise # Re-raise to be caught by outer handler + try: + while True: + try: + for chunk in research_agent.stream( + {"messages": [HumanMessage(content=research_prompt)]}, + config + ): + print_agent_output(chunk) + break + except ChatAnthropic.InternalServerError as e: + print(f"Encountered Anthropic Internal Server Error: {e}. Retrying...") + continue + except TaskCompletedException as e: + print_stage_header("Task Completed") + raise # Re-raise to be caught by outer handler - # Run any research subtasks - run_research_subtasks(base_task, config) - - # For informational queries, summarize findings - if is_informational_query(): - summarize_research_findings(base_task, config) - else: - # Only proceed with planning and implementation if not an informational query - print_stage_header("Planning Stage") - planning_prompt = PLANNING_PROMPT.format( - research_notes=get_memory_value('research_notes'), - key_facts=get_memory_value('key_facts'), - key_snippets=get_memory_value('key_snippets'), - base_task=base_task, - related_files="\n".join(related_files) - ) + # Run any research subtasks + run_research_subtasks(base_task, config) + + # For informational queries, summarize findings + if is_informational_query(): + summarize_research_findings(base_task, config) + else: + # Only proceed with planning and implementation if not an informational query + print_stage_header("Planning Stage") + planning_prompt = PLANNING_PROMPT.format( + research_notes=get_memory_value('research_notes'), + key_facts=get_memory_value('key_facts'), + key_snippets=get_memory_value('key_snippets'), + base_task=base_task, + related_files="\n".join(related_files) + ) - # Run planning agent - while True: - try: - for chunk in planning_agent.stream( - {"messages": [HumanMessage(content=planning_prompt)]}, - config - ): - print_agent_output(chunk) - break - except ChatAnthropic.InternalServerError as e: - print(f"Encountered Anthropic Internal Server Error: {e}. Retrying...") - continue + # Run planning agent + while True: + try: + for chunk in planning_agent.stream( + {"messages": [HumanMessage(content=planning_prompt)]}, + config + ): + print_agent_output(chunk) + break + except ChatAnthropic.InternalServerError as e: + print(f"Encountered Anthropic Internal Server Error: {e}. Retrying...") + continue - # Run implementation stage with task-specific agents - run_implementation_stage( - base_task, - get_memory_value('tasks'), - get_memory_value('plan'), - related_files - ) - except TaskCompletedException: - review_tech_debt(model) - sys.exit(0) + # Run implementation stage with task-specific agents + run_implementation_stage( + base_task, + get_memory_value('tasks'), + get_memory_value('plan'), + related_files + ) + except TaskCompletedException: + sys.exit(0) + finally: + # Show tech debt notification only when appropriate + if (check_tech_debt_notes() and + not getattr(args, 'review_tech_debt', False) and + not getattr(args, 'clear_tech_debt', False)): + console.print(Panel( + "[bold]Technical debt notes exist.[/]\n[dim italic]Use --review-tech-debt to review them.[/]", + border_style=BORDER_STYLE, + title=f"{TECH_DEBT_NOTE_EMOJI} Tech Debt" + )) if __name__ == "__main__": main() diff --git a/ra_aid/tools/expert.py b/ra_aid/tools/expert.py index 3110b98..d95f09c 100644 --- a/ra_aid/tools/expert.py +++ b/ra_aid/tools/expert.py @@ -21,6 +21,8 @@ def emit_expert_context(context: str) -> str: Err on the side of adding more context rather than less. + You must give the complete contents. + Expert context will be reset after the ask_expert tool is called. Args: @@ -29,9 +31,13 @@ def emit_expert_context(context: str) -> str: Returns: Confirmation message """ - global expert_context expert_context.append(context) - return f"Added context: {context}" + + # Create and display status panel + panel_content = f"Added expert context ({len(context)} characters)" + console.print(Panel(panel_content, title="Expert Context", border_style="blue")) + + return f"Context added." @tool("ask_expert") def ask_expert(question: str) -> str: diff --git a/ra_aid/tools/note_tech_debt.py b/ra_aid/tools/note_tech_debt.py index 8ea07ed..769b719 100644 --- a/ra_aid/tools/note_tech_debt.py +++ b/ra_aid/tools/note_tech_debt.py @@ -6,6 +6,11 @@ from langchain_core.tools import tool from rich.console import Console from rich.panel import Panel +# Rich styling constants for tech debt UI +BORDER_STYLE = "bright_blue" +TECH_DEBT_NOTE_EMOJI = "๐Ÿ“" +TECH_DEBT_CLEANUP_EMOJI = "๐Ÿงน" + MAX_NOTES = 10 # Maximum number of tech debt notes before cleanup warning console = Console() @@ -60,8 +65,8 @@ def note_tech_debt( [dim italic]The cleanup agent will analyze note contents and suggest which ones to purge.[/dim italic] """, - title="๐Ÿงน Tech Debt Cleanup", - border_style="bright_blue" + title=f"{TECH_DEBT_CLEANUP_EMOJI} Tech Debt Cleanup", + border_style=BORDER_STYLE ) ) @@ -83,8 +88,8 @@ def note_tech_debt( console.print( Panel( f"Created Tech Debt Note #{next_num} at {note_path}", - title="๐Ÿ“ Tech Debt Note", - border_style="bright_blue" + title=f"{TECH_DEBT_NOTE_EMOJI} Tech Debt Note", + border_style=BORDER_STYLE ) )