aider/local_analytics/run_aider_ask_test.py
flanker e8bee42d76 feat: Local Analytics Dashboard for Aider
feat: Initialize LocalAnalyticsCollector in main.py

feat: Display session data in local analytics dashboard

fix: Use cumulative data from last interaction for dashboard stats

fix: Extract initial query from diffs in local analytics collector.
2025-05-14 20:39:13 +05:30

184 lines
7.5 KiB
Python

import subprocess
import time
import os
import sys
import threading
import queue
# Define the aider command
# Use the full path to the .env file
# Assumes the script is run from the project root or a location where
# ~/Dev/aider/.env is the correct path.
# Using sys.executable ensures the script runs aider with the same python env.
aider_command = [
sys.executable,
"-m", "aider",
"--env-file", os.path.expanduser("~/Dev/aider/.env")
]
# Inputs to send to aider
inputs = [
"/ask",
"what is the reflection error",
"/exit"
]
# Expected prompts (as bytes, since we read bytes)
# Use strip() because rich might add spaces or other control characters
MAIN_PROMPT = b"> "
EDIT_PROMPT = b"Apply edits? (y/n/commit/diff/quit) "
def enqueue_output(out, queue):
"""Helper function to read output from a stream and put it in a queue."""
# Read line by line
for line in iter(out.readline, b''):
queue.put(line)
out.close()
def run_aider_session():
print(f"[SCRIPT] Starting aider with command: {' '.join(aider_command)}")
# Start the subprocess
# Use bufsize=1 for line buffering
# universal_newlines=False to read bytes and reliably detect byte prompts
# stderr is also piped as rich often prints to stderr
process = subprocess.Popen(
aider_command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
bufsize=1,
universal_newlines=False
)
# Queues for stdout and stderr
q_stdout = queue.Queue()
q_stderr = queue.Queue()
# Start threads to read stdout and stderr asynchronously
t_stdout = threading.Thread(target=enqueue_output, args=(process.stdout, q_stdout))
t_stderr = threading.Thread(target=enqueue_output, args=(process.stderr, q_stderr))
t_stdout.daemon = True # Thread dies with the main program
t_stderr.daemon = True
t_stdout.start()
t_stderr.start()
# Give aider a moment to start and print initial messages
time.sleep(3) # Increased initial sleep slightly
current_input_index = 0
# State machine: WAITING_FOR_MAIN_PROMPT, WAITING_FOR_RESPONSE
state = "WAITING_FOR_MAIN_PROMPT"
print(f"[SCRIPT] Initial state: {state}")
try:
# Continue as long as the process is running OR there is output in the queues
while process.poll() is None or not q_stdout.empty() or not q_stderr.empty():
try:
# Get a line from stdout queue with a timeout
# A small timeout allows the loop to check process.poll() and stderr queue
line = q_stdout.get(timeout=0.05)
sys.stdout.buffer.write(line)
sys.stdout.buffer.flush()
# Check for prompts based on the state
if state == "WAITING_FOR_MAIN_PROMPT":
# Check if the line ends with the main prompt bytes (after stripping)
if line.strip().endswith(MAIN_PROMPT.strip()):
print("\n[SCRIPT] Detected main prompt.")
if current_input_index < len(inputs):
command = inputs[current_input_index]
print(f"[SCRIPT] Sending: {command}")
process.stdin.write((command + "\n").encode()) # Encode string to bytes
process.stdin.flush()
current_input_index += 1
state = "WAITING_FOR_RESPONSE" # After sending input, wait for response/next prompt
print(f"[SCRIPT] State transition: {state}")
else:
# Should not happen if /exit is the last input, but as a safeguard
print("[SCRIPT] No more inputs defined, waiting for process exit.")
state = "SESSION_COMPLETE"
print(f"[SCRIPT] State transition: {state}")
elif state == "WAITING_FOR_RESPONSE":
# While waiting for response, we might see an edit prompt or the main prompt
if line.strip().endswith(EDIT_PROMPT.strip()):
print("\n[SCRIPT] Detected edit prompt.")
print("[SCRIPT] Sending: n")
process.stdin.write(b"n\n") # Send 'n' to decline edits
process.stdin.flush()
# Stay in WAITING_FOR_RESPONSE state, as declining might lead to another prompt
print(f"[SCRIPT] State remains: {state}")
elif line.strip().endswith(MAIN_PROMPT.strip()):
print("\n[SCRIPT] Detected main prompt (while waiting for response).")
# Response finished, now ready for next main input
state = "WAITING_FOR_MAIN_PROMPT"
print(f"[SCRIPT] State transition: {state}")
except queue.Empty:
# No output from stdout, check stderr queue
try:
err_line = q_stderr.get(timeout=0.01)
sys.stderr.buffer.write(err_line)
sys.stderr.buffer.flush()
except queue.Empty:
# No output from either queue, check if process is still running
if process.poll() is not None:
# Process exited and queues are empty, we are done
print("[SCRIPT] Process exited and queues are empty.")
break
# If process is still running but no output, just continue loop and wait
# Add a small sleep to prevent tight loop if process is slow to produce output
time.sleep(0.01)
# End of while loop: process exited and queues are drained
except Exception as e:
print(f"[SCRIPT] An error occurred: {e}")
# Attempt to read remaining output before terminating
try:
# Give threads a moment to finish putting data in queues
t_stdout.join(timeout=1)
t_stderr.join(timeout=1)
# Drain queues
while not q_stdout.empty():
sys.stdout.buffer.write(q_stdout.get_nowait())
sys.stdout.buffer.flush()
while not q_stderr.empty():
sys.stderr.buffer.write(q_stderr.get_nowait())
sys.stdout.buffer.flush()
except Exception as e_drain:
print(f"[SCRIPT] Error draining queues: {e_drain}")
if process.poll() is None:
print("[SCRIPT] Terminating process...")
process.terminate() # Ensure process is terminated on error
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
print("[SCRIPT] Process did not terminate, killing...")
process.kill()
process.wait()
finally:
# Ensure process is waited upon if not already
if process.poll() is None:
print("[SCRIPT] Waiting for process to finish...")
process.wait()
# Final drain of queues just in case
while not q_stdout.empty():
sys.stdout.buffer.write(q_stdout.get_nowait())
sys.stdout.buffer.flush()
while not q_stderr.empty():
sys.stderr.buffer.write(q_stderr.get_nowait())
sys.stderr.buffer.flush()
print(f"[SCRIPT] Aider process finished with return code {process.returncode}")
if __name__ == "__main__":
run_aider_session()