tech debt review

This commit is contained in:
AI Christianson 2024-12-11 19:05:04 -05:00
parent 053a70c602
commit 14abec9735
3 changed files with 191 additions and 81 deletions

View File

@ -4,6 +4,8 @@ import glob
import os import os
import sys import sys
import shutil import shutil
from rich.panel import Panel
from rich.console import Console
from langchain_anthropic import ChatAnthropic from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage from langchain_core.messages import HumanMessage
from langgraph.checkpoint.memory import MemorySaver from langgraph.checkpoint.memory import MemorySaver
@ -16,6 +18,7 @@ from ra_aid.tools import (
request_implementation, read_file_tool, emit_research_subtask, request_implementation, read_file_tool, emit_research_subtask,
fuzzy_find_project_files, ripgrep_search, list_directory_tree 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.tools.memory import _global_memory
from ra_aid import print_agent_output, print_stage_header, print_task_header from ra_aid import print_agent_output, print_stage_header, print_task_header
from ra_aid.tools.programmer import related_files from ra_aid.tools.programmer import related_files
@ -40,7 +43,6 @@ Examples:
parser.add_argument( parser.add_argument(
'-m', '--message', '-m', '--message',
type=str, type=str,
required=True,
help='The task or query to be executed by the agent' help='The task or query to be executed by the agent'
) )
parser.add_argument( parser.add_argument(
@ -53,8 +55,21 @@ Examples:
action='store_true', action='store_true',
help='Skip interactive approval for shell commands' 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() return parser.parse_args()
# Create console instance
console = Console()
# Create the base model # Create the base model
model = ChatAnthropic(model_name="claude-3-5-sonnet-20241022") 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...") print(f"Encountered Anthropic Internal Server Error: {e}. Retrying...")
continue 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(): def validate_environment():
"""Validate required environment variables and dependencies.""" """Validate required environment variables and dependencies."""
missing = [] missing = []
@ -237,12 +269,17 @@ def validate_environment():
print(f"- {error}", file=sys.stderr) print(f"- {error}", file=sys.stderr)
sys.exit(1) sys.exit(1)
def review_tech_debt(model) -> None: def review_tech_debt() -> None:
"""Review any technical debt notes collected during execution.""" """Review any technical debt notes collected during execution."""
tech_debt_dir = '.ra-aid/tech-debt' tech_debt_dir = '.ra-aid/tech-debt'
tech_debt_files = glob.glob(os.path.join(tech_debt_dir, '*.md')) tech_debt_files = glob.glob(os.path.join(tech_debt_dir, '*.md'))
if not tech_debt_files: 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 return
print_stage_header("Technical Debt Review") print_stage_header("Technical Debt Review")
@ -252,8 +289,26 @@ def review_tech_debt(model) -> None:
for file_path in tech_debt_files: for file_path in tech_debt_files:
with open(file_path, 'r') as file: with open(file_path, 'r') as file:
content = file.read() content = file.read()
tech_debt_contents.append("\n")
tech_debt_contents.append(content) 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 # Analyze the tech debt notes
prompt = f"""Review the following technical debt notes collected during program execution: prompt = f"""Review the following technical debt notes collected during program execution:
@ -264,11 +319,15 @@ Please provide a brief, focused analysis:
2. Highlight high-impact items 2. Highlight high-impact items
3. Suggest a rough priority order 3. Suggest a rough priority order
Keep the response concise and actionable. 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: while True:
try: try:
for chunk in model.stream( for chunk in tech_debt_agent.stream(
{"messages": [HumanMessage(content=prompt)]}, {"messages": [HumanMessage(content=prompt)]},
{"configurable": {"thread_id": "tech-debt"}, "recursion_limit": 100} {"configurable": {"thread_id": "tech-debt"}, "recursion_limit": 100}
): ):
@ -278,88 +337,128 @@ Keep the response concise and actionable.
print(f"Encountered Anthropic Internal Server Error: {e}. Retrying...") print(f"Encountered Anthropic Internal Server Error: {e}. Retrying...")
continue continue
# Exit immediately after tech debt review
sys.exit(0)
def main(): def main():
"""Main entry point for the ra-aid command line tool.""" """Main entry point for the ra-aid command line tool."""
try: try:
validate_environment() try:
args = parse_arguments() validate_environment()
base_task = args.message args = parse_arguments()
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 # Validate message is provided when needed
_global_memory['config'] = config 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)
# Run research stage # Handle clear tech debt request early
print_stage_header("Research Stage") if args.clear_tech_debt:
research_prompt = f"""User query: {base_task} --keep it simple 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} {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.'}""" 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: try:
while True: while True:
try: try:
for chunk in research_agent.stream( for chunk in research_agent.stream(
{"messages": [HumanMessage(content=research_prompt)]}, {"messages": [HumanMessage(content=research_prompt)]},
config config
): ):
print_agent_output(chunk) print_agent_output(chunk)
break break
except ChatAnthropic.InternalServerError as e: except ChatAnthropic.InternalServerError as e:
print(f"Encountered Anthropic Internal Server Error: {e}. Retrying...") print(f"Encountered Anthropic Internal Server Error: {e}. Retrying...")
continue continue
except TaskCompletedException as e: except TaskCompletedException as e:
print_stage_header("Task Completed") print_stage_header("Task Completed")
raise # Re-raise to be caught by outer handler raise # Re-raise to be caught by outer handler
# Run any research subtasks # Run any research subtasks
run_research_subtasks(base_task, config) run_research_subtasks(base_task, config)
# For informational queries, summarize findings # For informational queries, summarize findings
if is_informational_query(): if is_informational_query():
summarize_research_findings(base_task, config) summarize_research_findings(base_task, config)
else: else:
# Only proceed with planning and implementation if not an informational query # Only proceed with planning and implementation if not an informational query
print_stage_header("Planning Stage") print_stage_header("Planning Stage")
planning_prompt = PLANNING_PROMPT.format( planning_prompt = PLANNING_PROMPT.format(
research_notes=get_memory_value('research_notes'), research_notes=get_memory_value('research_notes'),
key_facts=get_memory_value('key_facts'), key_facts=get_memory_value('key_facts'),
key_snippets=get_memory_value('key_snippets'), key_snippets=get_memory_value('key_snippets'),
base_task=base_task, base_task=base_task,
related_files="\n".join(related_files) related_files="\n".join(related_files)
) )
# Run planning agent # Run planning agent
while True: while True:
try: try:
for chunk in planning_agent.stream( for chunk in planning_agent.stream(
{"messages": [HumanMessage(content=planning_prompt)]}, {"messages": [HumanMessage(content=planning_prompt)]},
config config
): ):
print_agent_output(chunk) print_agent_output(chunk)
break break
except ChatAnthropic.InternalServerError as e: except ChatAnthropic.InternalServerError as e:
print(f"Encountered Anthropic Internal Server Error: {e}. Retrying...") print(f"Encountered Anthropic Internal Server Error: {e}. Retrying...")
continue continue
# Run implementation stage with task-specific agents # Run implementation stage with task-specific agents
run_implementation_stage( run_implementation_stage(
base_task, base_task,
get_memory_value('tasks'), get_memory_value('tasks'),
get_memory_value('plan'), get_memory_value('plan'),
related_files related_files
) )
except TaskCompletedException: except TaskCompletedException:
review_tech_debt(model) sys.exit(0)
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__": if __name__ == "__main__":
main() main()

View File

@ -21,6 +21,8 @@ def emit_expert_context(context: str) -> str:
Err on the side of adding more context rather than less. 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. Expert context will be reset after the ask_expert tool is called.
Args: Args:
@ -29,9 +31,13 @@ def emit_expert_context(context: str) -> str:
Returns: Returns:
Confirmation message Confirmation message
""" """
global expert_context
expert_context.append(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") @tool("ask_expert")
def ask_expert(question: str) -> str: def ask_expert(question: str) -> str:

View File

@ -6,6 +6,11 @@ from langchain_core.tools import tool
from rich.console import Console from rich.console import Console
from rich.panel import Panel 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 MAX_NOTES = 10 # Maximum number of tech debt notes before cleanup warning
console = Console() 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] [dim italic]The cleanup agent will analyze note contents and suggest which ones to purge.[/dim italic]
""", """,
title="🧹 Tech Debt Cleanup", title=f"{TECH_DEBT_CLEANUP_EMOJI} Tech Debt Cleanup",
border_style="bright_blue" border_style=BORDER_STYLE
) )
) )
@ -83,8 +88,8 @@ def note_tech_debt(
console.print( console.print(
Panel( Panel(
f"Created Tech Debt Note #{next_num} at {note_path}", f"Created Tech Debt Note #{next_num} at {note_path}",
title="📝 Tech Debt Note", title=f"{TECH_DEBT_NOTE_EMOJI} Tech Debt Note",
border_style="bright_blue" border_style=BORDER_STYLE
) )
) )