Spaces:
Sleeping
Sleeping
Upload 17 files
Browse files- .flake8 +6 -0
- .gitignore +10 -0
- .pre-commit-config.yaml +33 -0
- Dockerfile +10 -0
- app/core/config.py +44 -0
- app/main.py +140 -0
- app/models/__init__.py +1 -0
- app/models/admin.py +36 -0
- app/routers/__init__.py +1 -0
- app/routers/admin.py +117 -0
- app/routers/aider.py +357 -0
- app/services/aider_service.py +272 -0
- app/services/session_manager.py +49 -0
- app/utils/output_parser.py +101 -0
- client.py +169 -0
- mypy.ini +17 -0
- requirements.txt +8 -0
.flake8
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[flake8]
|
| 2 |
+
exclude = fixtures, __jac_gen__, build, examples, vendor, ./server/src/tests/*, temp*
|
| 3 |
+
plugins = flake8_import_order, flake8_docstrings, flake8_comprehensions, flake8_bugbear, flake8_annotations, pep8_naming, flake8_simplify
|
| 4 |
+
max-line-length = 120
|
| 5 |
+
ignore = E203, W503, ANN101, ANN102, I201, ANN401, D401, SIM115, E501
|
| 6 |
+
per-file-ignores = server/src/utils/onboarding_questions.py:E501
|
.gitignore
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.aider*
|
| 2 |
+
.env*
|
| 3 |
+
__pycache__
|
| 4 |
+
*.pyc
|
| 5 |
+
*.pyo
|
| 6 |
+
*.pyd
|
| 7 |
+
*.pyw
|
| 8 |
+
*.pyz
|
| 9 |
+
*.pywz
|
| 10 |
+
aider_websocket_*
|
.pre-commit-config.yaml
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
repos:
|
| 2 |
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
| 3 |
+
rev: v2.3.0
|
| 4 |
+
hooks:
|
| 5 |
+
- id: check-yaml
|
| 6 |
+
args: [--allow-multiple-documents]
|
| 7 |
+
- id: check-json
|
| 8 |
+
- id: trailing-whitespace
|
| 9 |
+
- repo: https://github.com/psf/black
|
| 10 |
+
rev: 24.1.1
|
| 11 |
+
hooks:
|
| 12 |
+
- id: black
|
| 13 |
+
- hooks:
|
| 14 |
+
- args:
|
| 15 |
+
- --profile
|
| 16 |
+
- black
|
| 17 |
+
id: isort
|
| 18 |
+
repo: https://github.com/pycqa/isort
|
| 19 |
+
rev: 5.12.0
|
| 20 |
+
- repo: https://github.com/PyCQA/flake8
|
| 21 |
+
rev: 6.1.0
|
| 22 |
+
hooks:
|
| 23 |
+
- id: flake8
|
| 24 |
+
additional_dependencies: [pep8-naming, flake8_docstrings, flake8_comprehensions, flake8_bugbear, flake8_annotations, flake8_simplify]
|
| 25 |
+
exclude: "examples|vendor"
|
| 26 |
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
| 27 |
+
rev: v1.8.0
|
| 28 |
+
hooks:
|
| 29 |
+
- id: mypy
|
| 30 |
+
exclude: 'venv|.conda|.git|.vscode|__pycache__|tests|examples|vendor|build|dist|.*.egg-info|.*.egg|temp*'
|
| 31 |
+
args:
|
| 32 |
+
- --follow-imports=silent
|
| 33 |
+
- --ignore-missing-imports
|
Dockerfile
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY . /app
|
| 6 |
+
|
| 7 |
+
RUN pip install -r requirements.txt
|
| 8 |
+
RUN aider-install
|
| 9 |
+
|
| 10 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
app/core/config.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration settings for the application."""
|
| 2 |
+
|
| 3 |
+
from typing import List
|
| 4 |
+
|
| 5 |
+
from pydantic_settings import BaseSettings
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class Settings(BaseSettings):
|
| 9 |
+
"""Settings for the application."""
|
| 10 |
+
|
| 11 |
+
OPENAI_API_KEY: str
|
| 12 |
+
AIDER_MODEL: str = "gpt-4o"
|
| 13 |
+
TEMP_DIR_PREFIX: str = "aider_websocket_"
|
| 14 |
+
ADMIN_API_KEY: str
|
| 15 |
+
AZURE_API_KEY: str
|
| 16 |
+
AZURE_API_VERSION: str
|
| 17 |
+
AZURE_API_BASE: str
|
| 18 |
+
# AIDER_MODEL_METADATA_FILE: str = ".aider.model.metadata.json"
|
| 19 |
+
# AIDER_GITIGNORE: bool = True
|
| 20 |
+
EXCLUDE_PATTERNS: List[str] = [
|
| 21 |
+
"__pycache__",
|
| 22 |
+
"*.pyc",
|
| 23 |
+
"*.pyo",
|
| 24 |
+
"*.pyd",
|
| 25 |
+
".git",
|
| 26 |
+
".gitignore",
|
| 27 |
+
".env",
|
| 28 |
+
".DS_Store",
|
| 29 |
+
"*.log",
|
| 30 |
+
".pytest_cache",
|
| 31 |
+
".coverage",
|
| 32 |
+
".mypy_cache",
|
| 33 |
+
".idea",
|
| 34 |
+
".vscode",
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
class Config:
|
| 38 |
+
"""Settings configuration."""
|
| 39 |
+
|
| 40 |
+
env_file = ".env"
|
| 41 |
+
case_sensitive = True
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
settings = Settings()
|
app/main.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Main entry point for the FastAPI application."""
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
| 7 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
+
|
| 9 |
+
from app.routers import admin, aider
|
| 10 |
+
from app.services.session_manager import session_manager
|
| 11 |
+
|
| 12 |
+
RESTRICTED_COMMANDS = {
|
| 13 |
+
"/model", # Prevents model switching
|
| 14 |
+
"/tokens",
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
app = FastAPI(title="Aider WebSocket Server")
|
| 18 |
+
|
| 19 |
+
app.add_middleware(
|
| 20 |
+
CORSMiddleware,
|
| 21 |
+
allow_origins=["*"], # Frontend URL
|
| 22 |
+
allow_credentials=True, # Required for WebSocket with credentials
|
| 23 |
+
allow_methods=["*"], # Allow all HTTP methods (GET, POST, etc.)
|
| 24 |
+
allow_headers=["*"], # Allow all headers
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
app.include_router(admin.router)
|
| 28 |
+
app.include_router(aider.router)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@app.websocket("/ws")
|
| 32 |
+
async def websocket_endpoint(websocket: WebSocket) -> None:
|
| 33 |
+
"""Websocket endpoint for handling commands and sessions."""
|
| 34 |
+
await websocket.accept()
|
| 35 |
+
|
| 36 |
+
try:
|
| 37 |
+
data = await websocket.receive_json()
|
| 38 |
+
repo_url = data.get("repo_url")
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
session_id = await session_manager.create_session(repo_url=repo_url)
|
| 42 |
+
await websocket.send_json(
|
| 43 |
+
{"type": "status", "content": f"Session created with ID: {session_id}"}
|
| 44 |
+
)
|
| 45 |
+
except Exception as e:
|
| 46 |
+
await websocket.send_json(
|
| 47 |
+
{"type": "error", "content": f"Failed to create session: {str(e)}"}
|
| 48 |
+
)
|
| 49 |
+
return
|
| 50 |
+
|
| 51 |
+
aider_service = session_manager.get_session(session_id)
|
| 52 |
+
|
| 53 |
+
output_task = asyncio.create_task(aider_service.process_output(websocket))
|
| 54 |
+
|
| 55 |
+
while True:
|
| 56 |
+
try:
|
| 57 |
+
data = await websocket.receive_json()
|
| 58 |
+
command = data.get("command")
|
| 59 |
+
|
| 60 |
+
if command == "send_to_aider":
|
| 61 |
+
message = data.get("message", "")
|
| 62 |
+
if message.startswith("/"):
|
| 63 |
+
cmd = message.split()[0].lower()
|
| 64 |
+
if cmd in RESTRICTED_COMMANDS:
|
| 65 |
+
await websocket.send_json(
|
| 66 |
+
{
|
| 67 |
+
"type": "error",
|
| 68 |
+
"content": f"Command '{cmd}' is not allowed in this environment.",
|
| 69 |
+
}
|
| 70 |
+
)
|
| 71 |
+
continue
|
| 72 |
+
await aider_service.send_command(message)
|
| 73 |
+
elif command == "list_files":
|
| 74 |
+
files = await aider_service.list_files()
|
| 75 |
+
await websocket.send_json({"type": "files", "content": files})
|
| 76 |
+
elif command == "read_file":
|
| 77 |
+
filename = data.get("filename")
|
| 78 |
+
if filename:
|
| 79 |
+
content = await aider_service.read_file(filename)
|
| 80 |
+
await websocket.send_json(
|
| 81 |
+
{"type": "file_content", "content": content}
|
| 82 |
+
)
|
| 83 |
+
else:
|
| 84 |
+
await websocket.send_json(
|
| 85 |
+
{"type": "error", "content": "Filename not provided"}
|
| 86 |
+
)
|
| 87 |
+
elif command == "download":
|
| 88 |
+
zip_path = await aider_service.create_download_zip()
|
| 89 |
+
if zip_path:
|
| 90 |
+
try:
|
| 91 |
+
with open(zip_path, "rb") as f:
|
| 92 |
+
zip_content = f.read()
|
| 93 |
+
await websocket.send_bytes(zip_content)
|
| 94 |
+
finally:
|
| 95 |
+
if os.path.exists(zip_path):
|
| 96 |
+
os.remove(zip_path)
|
| 97 |
+
else:
|
| 98 |
+
await websocket.send_json(
|
| 99 |
+
{
|
| 100 |
+
"type": "error",
|
| 101 |
+
"content": "Failed to create download zip",
|
| 102 |
+
}
|
| 103 |
+
)
|
| 104 |
+
elif command.startswith("/"):
|
| 105 |
+
cmd = command.split()[0].lower()
|
| 106 |
+
if cmd in RESTRICTED_COMMANDS:
|
| 107 |
+
await websocket.send_json(
|
| 108 |
+
{
|
| 109 |
+
"type": "error",
|
| 110 |
+
"content": f"Command '{cmd}' is not allowed in this environment.",
|
| 111 |
+
}
|
| 112 |
+
)
|
| 113 |
+
continue
|
| 114 |
+
|
| 115 |
+
command_str = command
|
| 116 |
+
if "args" in data:
|
| 117 |
+
args = data.get("args", [])
|
| 118 |
+
command_str += " " + " ".join(args)
|
| 119 |
+
await aider_service.send_command(command_str)
|
| 120 |
+
else:
|
| 121 |
+
await websocket.send_json(
|
| 122 |
+
{"type": "error", "content": "Unknown command"}
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
except WebSocketDisconnect:
|
| 126 |
+
break
|
| 127 |
+
except Exception as e:
|
| 128 |
+
await websocket.send_json({"type": "error", "content": str(e)})
|
| 129 |
+
|
| 130 |
+
except Exception as e:
|
| 131 |
+
await websocket.send_json({"type": "error", "content": str(e)})
|
| 132 |
+
finally:
|
| 133 |
+
output_task.cancel()
|
| 134 |
+
await session_manager.cleanup_session(session_id)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
@app.on_event("shutdown")
|
| 138 |
+
async def shutdown_event() -> None:
|
| 139 |
+
"""Clean up all sessions when the server shuts down."""
|
| 140 |
+
await session_manager.cleanup_all_sessions()
|
app/models/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Initialize the models package."""
|
app/models/admin.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Admin API models."""
|
| 2 |
+
|
| 3 |
+
from typing import List, Optional
|
| 4 |
+
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class SessionInfo(BaseModel):
|
| 9 |
+
"""Session information."""
|
| 10 |
+
|
| 11 |
+
id: str
|
| 12 |
+
path: str
|
| 13 |
+
created_at: float
|
| 14 |
+
size: int
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class SessionList(BaseModel):
|
| 18 |
+
"""List of sessions."""
|
| 19 |
+
|
| 20 |
+
sessions: List[SessionInfo]
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class ConfigUpdate(BaseModel):
|
| 24 |
+
"""Configuration update model."""
|
| 25 |
+
|
| 26 |
+
openai_api_key: Optional[str] = None
|
| 27 |
+
aider_model: Optional[str] = None
|
| 28 |
+
temp_dir_prefix: Optional[str] = None
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class ConfigResponse(BaseModel):
|
| 32 |
+
"""Configuration response model."""
|
| 33 |
+
|
| 34 |
+
openai_api_key: Optional[str]
|
| 35 |
+
aider_model: str
|
| 36 |
+
temp_dir_prefix: str
|
app/routers/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Initializes the routers module."""
|
app/routers/admin.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Admin router module for managing Aider sessions and configurations."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import shutil
|
| 5 |
+
import tempfile
|
| 6 |
+
|
| 7 |
+
from fastapi import APIRouter, Depends, HTTPException, Security
|
| 8 |
+
from fastapi.security import APIKeyHeader
|
| 9 |
+
|
| 10 |
+
from app.core.config import settings
|
| 11 |
+
from app.models.admin import ConfigResponse, ConfigUpdate, SessionInfo, SessionList
|
| 12 |
+
|
| 13 |
+
router = APIRouter(prefix="/admin", tags=["admin"])
|
| 14 |
+
|
| 15 |
+
# Security
|
| 16 |
+
API_KEY_NAME = "X-API-Key"
|
| 17 |
+
api_key_header = APIKeyHeader(name=API_KEY_NAME)
|
| 18 |
+
|
| 19 |
+
# Define dependency outside of function signature
|
| 20 |
+
verify_admin_dep = Security(api_key_header)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
async def verify_admin_key(api_key: str = verify_admin_dep) -> str:
|
| 24 |
+
"""Verify the admin API key."""
|
| 25 |
+
if api_key != settings.ADMIN_API_KEY:
|
| 26 |
+
raise HTTPException(status_code=403, detail="Invalid API key")
|
| 27 |
+
return api_key
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@router.get("/sessions", response_model=SessionList)
|
| 31 |
+
async def list_sessions(api_key: str = Depends(verify_admin_key)) -> SessionList:
|
| 32 |
+
"""List all active Aider sessions."""
|
| 33 |
+
sessions = []
|
| 34 |
+
temp_dir = tempfile.gettempdir()
|
| 35 |
+
|
| 36 |
+
for dir_name in os.listdir(temp_dir):
|
| 37 |
+
if dir_name.startswith(settings.TEMP_DIR_PREFIX):
|
| 38 |
+
session_path = os.path.join(temp_dir, dir_name)
|
| 39 |
+
if os.path.isdir(session_path):
|
| 40 |
+
sessions.append(
|
| 41 |
+
SessionInfo(
|
| 42 |
+
id=dir_name,
|
| 43 |
+
path=session_path,
|
| 44 |
+
created_at=os.path.getctime(session_path),
|
| 45 |
+
size=sum(
|
| 46 |
+
os.path.getsize(os.path.join(session_path, f))
|
| 47 |
+
for f in os.listdir(session_path)
|
| 48 |
+
if os.path.isfile(os.path.join(session_path, f))
|
| 49 |
+
),
|
| 50 |
+
)
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
return SessionList(sessions=sessions)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@router.delete("/sessions/{session_id}")
|
| 57 |
+
async def delete_session(
|
| 58 |
+
session_id: str, api_key: str = Depends(verify_admin_key)
|
| 59 |
+
) -> dict:
|
| 60 |
+
"""Delete a specific Aider session."""
|
| 61 |
+
session_path = os.path.join(tempfile.gettempdir(), session_id)
|
| 62 |
+
|
| 63 |
+
if not os.path.exists(session_path):
|
| 64 |
+
raise HTTPException(status_code=404, detail="Session not found")
|
| 65 |
+
|
| 66 |
+
try:
|
| 67 |
+
shutil.rmtree(session_path)
|
| 68 |
+
return {"message": "Session deleted successfully"}
|
| 69 |
+
except Exception as e:
|
| 70 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@router.delete("/sessions")
|
| 74 |
+
async def delete_all_sessions(api_key: str = Depends(verify_admin_key)) -> dict:
|
| 75 |
+
"""Delete all Aider sessions."""
|
| 76 |
+
temp_dir = tempfile.gettempdir()
|
| 77 |
+
deleted_count = 0
|
| 78 |
+
|
| 79 |
+
for dir_name in os.listdir(temp_dir):
|
| 80 |
+
if dir_name.startswith(settings.TEMP_DIR_PREFIX):
|
| 81 |
+
session_path = os.path.join(temp_dir, dir_name)
|
| 82 |
+
if os.path.isdir(session_path):
|
| 83 |
+
try:
|
| 84 |
+
shutil.rmtree(session_path)
|
| 85 |
+
deleted_count += 1
|
| 86 |
+
except Exception as e:
|
| 87 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 88 |
+
|
| 89 |
+
return {"message": f"Deleted {deleted_count} sessions"}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
@router.get("/config", response_model=ConfigResponse)
|
| 93 |
+
async def get_config(api_key: str = Depends(verify_admin_key)) -> ConfigResponse:
|
| 94 |
+
"""Get current configuration settings."""
|
| 95 |
+
masked_key = (
|
| 96 |
+
settings.OPENAI_API_KEY[:8] + "..." if settings.OPENAI_API_KEY else None
|
| 97 |
+
)
|
| 98 |
+
return ConfigResponse(
|
| 99 |
+
openai_api_key=masked_key,
|
| 100 |
+
aider_model=settings.AIDER_MODEL,
|
| 101 |
+
temp_dir_prefix=settings.TEMP_DIR_PREFIX,
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
@router.put("/config")
|
| 106 |
+
async def update_config(
|
| 107 |
+
config: ConfigUpdate, api_key: str = Depends(verify_admin_key)
|
| 108 |
+
) -> dict:
|
| 109 |
+
"""Update configuration settings."""
|
| 110 |
+
if config.openai_api_key:
|
| 111 |
+
settings.OPENAI_API_KEY = config.openai_api_key
|
| 112 |
+
if config.aider_model:
|
| 113 |
+
settings.AIDER_MODEL = config.aider_model
|
| 114 |
+
if config.temp_dir_prefix:
|
| 115 |
+
settings.TEMP_DIR_PREFIX = config.temp_dir_prefix
|
| 116 |
+
|
| 117 |
+
return {"message": "Configuration updated successfully"}
|
app/routers/aider.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Aider router module for managing AI development sessions."""
|
| 2 |
+
|
| 3 |
+
from typing import List, Optional
|
| 4 |
+
|
| 5 |
+
from fastapi import APIRouter, HTTPException
|
| 6 |
+
from pydantic import BaseModel
|
| 7 |
+
|
| 8 |
+
from app.services.session_manager import session_manager
|
| 9 |
+
|
| 10 |
+
router = APIRouter(prefix="/aider", tags=["aider"])
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class AiderCommand(BaseModel):
|
| 14 |
+
"""Command structure for Aider operations."""
|
| 15 |
+
|
| 16 |
+
command: str
|
| 17 |
+
args: Optional[List[str]] = None
|
| 18 |
+
session_id: str
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class FileOperation(BaseModel):
|
| 22 |
+
"""File operation structure for Aider."""
|
| 23 |
+
|
| 24 |
+
files: List[str]
|
| 25 |
+
session_id: str
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class ModelSwitch(BaseModel):
|
| 29 |
+
"""Model switching structure for Aider."""
|
| 30 |
+
|
| 31 |
+
model: str
|
| 32 |
+
session_id: str
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class TokenBudget(BaseModel):
|
| 36 |
+
"""Token budget structure for Aider."""
|
| 37 |
+
|
| 38 |
+
tokens: str
|
| 39 |
+
session_id: str
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class ReasoningEffort(BaseModel):
|
| 43 |
+
"""Reasoning effort structure for Aider."""
|
| 44 |
+
|
| 45 |
+
level: str
|
| 46 |
+
session_id: str
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class WebScrape(BaseModel):
|
| 50 |
+
"""Web scraping structure for Aider."""
|
| 51 |
+
|
| 52 |
+
url: str
|
| 53 |
+
session_id: str
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class CreateSession(BaseModel):
|
| 57 |
+
"""Session creation structure for Aider."""
|
| 58 |
+
|
| 59 |
+
repo_url: Optional[str] = None
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def get_session(session_id: str) -> None:
|
| 63 |
+
"""Helper function to get a session or raise HTTPException."""
|
| 64 |
+
session = session_manager.get_session(session_id)
|
| 65 |
+
if not session:
|
| 66 |
+
raise HTTPException(status_code=404, detail="Session not found")
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@router.post("/create-session")
|
| 70 |
+
async def create_session(session_params: Optional[CreateSession] = None) -> dict:
|
| 71 |
+
"""Create a new aider session, optionally with a cloned repository."""
|
| 72 |
+
try:
|
| 73 |
+
repo_url = None
|
| 74 |
+
if session_params and session_params.repo_url:
|
| 75 |
+
repo_url = session_params.repo_url
|
| 76 |
+
|
| 77 |
+
session_id = await session_manager.create_session(repo_url=repo_url)
|
| 78 |
+
return {"session_id": session_id, "message": "Session created successfully"}
|
| 79 |
+
except Exception as e:
|
| 80 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
@router.post("/add")
|
| 84 |
+
async def add_files(files: FileOperation) -> dict:
|
| 85 |
+
"""Add files to the chat session."""
|
| 86 |
+
get_session(files.session_id)
|
| 87 |
+
return {"command": "/add", "files": files.files}
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
@router.post("/architect")
|
| 91 |
+
async def architect_mode(command: AiderCommand) -> dict:
|
| 92 |
+
"""Enter architect/editor mode."""
|
| 93 |
+
get_session(command.session_id)
|
| 94 |
+
return {
|
| 95 |
+
"command": "/architect",
|
| 96 |
+
"prompt": command.args[0] if command.args else None,
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@router.post("/ask")
|
| 101 |
+
async def ask_mode(command: AiderCommand) -> dict:
|
| 102 |
+
"""Enter ask mode."""
|
| 103 |
+
get_session(command.session_id)
|
| 104 |
+
return {"command": "/ask", "prompt": command.args[0] if command.args else None}
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
@router.post("/chat-mode")
|
| 108 |
+
async def chat_mode(command: AiderCommand) -> dict:
|
| 109 |
+
"""Switch to a new chat mode."""
|
| 110 |
+
get_session(command.session_id)
|
| 111 |
+
return {"command": "/chat-mode", "mode": command.args[0] if command.args else None}
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
@router.post("/clear")
|
| 115 |
+
async def clear_chat(command: AiderCommand) -> dict:
|
| 116 |
+
"""Clear the chat history."""
|
| 117 |
+
get_session(command.session_id)
|
| 118 |
+
return {"command": "/clear"}
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
@router.post("/code")
|
| 122 |
+
async def code_mode(command: AiderCommand) -> dict:
|
| 123 |
+
"""Enter code mode."""
|
| 124 |
+
get_session(command.session_id)
|
| 125 |
+
return {"command": "/code", "prompt": command.args[0] if command.args else None}
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
@router.post("/commit")
|
| 129 |
+
async def commit_changes(command: AiderCommand) -> dict:
|
| 130 |
+
"""Commit changes to the repository."""
|
| 131 |
+
get_session(command.session_id)
|
| 132 |
+
return {"command": "/commit", "message": command.args[0] if command.args else None}
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
@router.post("/context")
|
| 136 |
+
async def context_mode(command: AiderCommand) -> dict:
|
| 137 |
+
"""Enter context mode."""
|
| 138 |
+
get_session(command.session_id)
|
| 139 |
+
return {"command": "/context", "prompt": command.args[0] if command.args else None}
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
@router.post("/copy")
|
| 143 |
+
async def copy_last_message(command: AiderCommand) -> dict:
|
| 144 |
+
"""Copy the last assistant message."""
|
| 145 |
+
get_session(command.session_id)
|
| 146 |
+
return {"command": "/copy"}
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
@router.post("/copy-context")
|
| 150 |
+
async def copy_context(command: AiderCommand) -> dict:
|
| 151 |
+
"""Copy the current chat context."""
|
| 152 |
+
get_session(command.session_id)
|
| 153 |
+
return {"command": "/copy-context"}
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
@router.post("/diff")
|
| 157 |
+
async def show_diff(command: AiderCommand) -> dict:
|
| 158 |
+
"""Show diff of changes."""
|
| 159 |
+
get_session(command.session_id)
|
| 160 |
+
return {"command": "/diff"}
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
@router.post("/drop")
|
| 164 |
+
async def drop_files(files: FileOperation) -> dict:
|
| 165 |
+
"""Remove files from chat session."""
|
| 166 |
+
get_session(files.session_id)
|
| 167 |
+
return {"command": "/drop", "files": files.files}
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
@router.post("/editor")
|
| 171 |
+
async def open_editor(command: AiderCommand) -> dict:
|
| 172 |
+
"""Open editor for prompt."""
|
| 173 |
+
get_session(command.session_id)
|
| 174 |
+
return {"command": "/editor", "prompt": command.args[0] if command.args else None}
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
@router.post("/editor-model")
|
| 178 |
+
async def switch_editor_model(model: ModelSwitch) -> dict:
|
| 179 |
+
"""Switch the editor model."""
|
| 180 |
+
get_session(model.session_id)
|
| 181 |
+
return {"command": "/editor-model", "model": model.model}
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
@router.post("/git")
|
| 185 |
+
async def git_command(command: AiderCommand) -> dict:
|
| 186 |
+
"""Run a git command."""
|
| 187 |
+
get_session(command.session_id)
|
| 188 |
+
return {"command": "/git", "git_command": command.args[0] if command.args else None}
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
@router.post("/help")
|
| 192 |
+
async def help_command(command: AiderCommand) -> dict:
|
| 193 |
+
"""Get help about aider."""
|
| 194 |
+
get_session(command.session_id)
|
| 195 |
+
return {"command": "/help", "question": command.args[0] if command.args else None}
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
@router.post("/lint")
|
| 199 |
+
async def lint_files(files: Optional[FileOperation] = None) -> dict:
|
| 200 |
+
"""Lint files."""
|
| 201 |
+
if files:
|
| 202 |
+
get_session(files.session_id)
|
| 203 |
+
return {"command": "/lint", "files": files.files if files else None}
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
@router.post("/load")
|
| 207 |
+
async def load_commands(command: AiderCommand) -> dict:
|
| 208 |
+
"""Load commands from a file."""
|
| 209 |
+
get_session(command.session_id)
|
| 210 |
+
return {"command": "/load", "file": command.args[0] if command.args else None}
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
@router.post("/ls")
|
| 214 |
+
async def list_files(command: AiderCommand) -> dict:
|
| 215 |
+
"""List all known files."""
|
| 216 |
+
get_session(command.session_id)
|
| 217 |
+
return {"command": "/ls"}
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
@router.post("/map")
|
| 221 |
+
async def show_map(command: AiderCommand) -> dict:
|
| 222 |
+
"""Show repository map."""
|
| 223 |
+
get_session(command.session_id)
|
| 224 |
+
return {"command": "/map"}
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
@router.post("/map-refresh")
|
| 228 |
+
async def refresh_map(command: AiderCommand) -> dict:
|
| 229 |
+
"""Refresh repository map."""
|
| 230 |
+
get_session(command.session_id)
|
| 231 |
+
return {"command": "/map-refresh"}
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
@router.post("/model")
|
| 235 |
+
async def switch_model(model: ModelSwitch) -> dict:
|
| 236 |
+
"""Switch the main model."""
|
| 237 |
+
get_session(model.session_id)
|
| 238 |
+
return {"command": "/model", "model": model.model}
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
@router.post("/models")
|
| 242 |
+
async def search_models(command: AiderCommand) -> dict:
|
| 243 |
+
"""Search available models."""
|
| 244 |
+
get_session(command.session_id)
|
| 245 |
+
return {"command": "/models", "query": command.args[0] if command.args else None}
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
@router.post("/multiline-mode")
|
| 249 |
+
async def toggle_multiline(command: AiderCommand) -> dict:
|
| 250 |
+
"""Toggle multiline mode."""
|
| 251 |
+
get_session(command.session_id)
|
| 252 |
+
return {"command": "/multiline-mode"}
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
@router.post("/paste")
|
| 256 |
+
async def paste_content(command: AiderCommand) -> dict:
|
| 257 |
+
"""Paste content from clipboard."""
|
| 258 |
+
get_session(command.session_id)
|
| 259 |
+
content = command.args[0] if command.args else None
|
| 260 |
+
name = command.args[1] if len(command.args) > 1 else None
|
| 261 |
+
return {"command": "/paste", "content": content, "name": name}
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
@router.post("/read-only")
|
| 265 |
+
async def set_read_only(files: FileOperation, read_only: bool = True) -> dict:
|
| 266 |
+
"""Set files as read-only."""
|
| 267 |
+
get_session(files.session_id)
|
| 268 |
+
return {"command": "/read-only", "files": files.files, "read_only": read_only}
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
@router.post("/reasoning-effort")
|
| 272 |
+
async def set_reasoning_effort(level: ReasoningEffort) -> dict:
|
| 273 |
+
"""Set reasoning effort level."""
|
| 274 |
+
get_session(level.session_id)
|
| 275 |
+
return {"command": "/reasoning-effort", "level": level.level}
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
@router.post("/reset")
|
| 279 |
+
async def reset_session(command: AiderCommand) -> dict:
|
| 280 |
+
"""Reset the session."""
|
| 281 |
+
get_session(command.session_id)
|
| 282 |
+
return {"command": "/reset"}
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
@router.post("/run")
|
| 286 |
+
async def run_command(command: AiderCommand) -> dict:
|
| 287 |
+
"""Run a shell command."""
|
| 288 |
+
get_session(command.session_id)
|
| 289 |
+
shell_command = command.args[0] if command.args else None
|
| 290 |
+
add_output = command.args[1] == "true" if len(command.args) > 1 else False
|
| 291 |
+
return {"command": "/run", "shell_command": shell_command, "add_output": add_output}
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
@router.post("/save")
|
| 295 |
+
async def save_commands(command: AiderCommand) -> dict:
|
| 296 |
+
"""Save commands to a file."""
|
| 297 |
+
get_session(command.session_id)
|
| 298 |
+
return {"command": "/save", "file": command.args[0] if command.args else None}
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
@router.post("/settings")
|
| 302 |
+
async def show_settings(command: AiderCommand) -> dict:
|
| 303 |
+
"""Show current settings."""
|
| 304 |
+
get_session(command.session_id)
|
| 305 |
+
return {"command": "/settings"}
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
@router.post("/test")
|
| 309 |
+
async def test_command(command: AiderCommand) -> dict:
|
| 310 |
+
"""Run a test command."""
|
| 311 |
+
get_session(command.session_id)
|
| 312 |
+
return {
|
| 313 |
+
"command": "/test",
|
| 314 |
+
"shell_command": command.args[0] if command.args else None,
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
@router.post("/think-tokens")
|
| 319 |
+
async def set_think_tokens(tokens: TokenBudget) -> dict:
|
| 320 |
+
"""Set thinking token budget."""
|
| 321 |
+
get_session(tokens.session_id)
|
| 322 |
+
return {"command": "/think-tokens", "tokens": tokens.tokens}
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
@router.post("/tokens")
|
| 326 |
+
async def show_tokens(command: AiderCommand) -> dict:
|
| 327 |
+
"""Show token usage."""
|
| 328 |
+
get_session(command.session_id)
|
| 329 |
+
return {"command": "/tokens"}
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
@router.post("/undo")
|
| 333 |
+
async def undo_last_commit(command: AiderCommand) -> dict:
|
| 334 |
+
"""Undo last commit."""
|
| 335 |
+
get_session(command.session_id)
|
| 336 |
+
return {"command": "/undo"}
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
@router.post("/voice")
|
| 340 |
+
async def record_voice(command: AiderCommand) -> dict:
|
| 341 |
+
"""Record and transcribe voice input."""
|
| 342 |
+
get_session(command.session_id)
|
| 343 |
+
return {"command": "/voice"}
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
@router.post("/weak-model")
|
| 347 |
+
async def switch_weak_model(model: ModelSwitch) -> dict:
|
| 348 |
+
"""Switch the weak model."""
|
| 349 |
+
get_session(model.session_id)
|
| 350 |
+
return {"command": "/weak-model", "model": model.model}
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
@router.post("/web")
|
| 354 |
+
async def scrape_webpage(url: WebScrape) -> dict:
|
| 355 |
+
"""Scrape a webpage."""
|
| 356 |
+
get_session(url.session_id)
|
| 357 |
+
return {"command": "/web", "url": url.url}
|
app/services/aider_service.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module for managing Aider sessions and interactions."""
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import fnmatch
|
| 5 |
+
import os
|
| 6 |
+
import re
|
| 7 |
+
import shutil
|
| 8 |
+
import signal
|
| 9 |
+
import tempfile
|
| 10 |
+
import zipfile
|
| 11 |
+
from typing import Optional
|
| 12 |
+
|
| 13 |
+
from ansi2html import Ansi2HTMLConverter
|
| 14 |
+
from fastapi import WebSocket
|
| 15 |
+
|
| 16 |
+
from app.core.config import settings
|
| 17 |
+
from app.utils.output_parser import AiderOutputParser
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class AiderService:
|
| 21 |
+
"""AiderService class to manage Aider sessions and interactions."""
|
| 22 |
+
|
| 23 |
+
def __init__(self) -> None:
|
| 24 |
+
"""Initialize the AiderService with default values."""
|
| 25 |
+
self.converter = Ansi2HTMLConverter()
|
| 26 |
+
self.output_parser = AiderOutputParser()
|
| 27 |
+
self.temp_dir: Optional[str] = None
|
| 28 |
+
self.aider_process: Optional[asyncio.subprocess.Process] = None
|
| 29 |
+
self.running_process: Optional[asyncio.subprocess.Process] = None
|
| 30 |
+
|
| 31 |
+
async def initialize(self, repo_url: Optional[str] = None) -> str:
|
| 32 |
+
"""Initialize a new Aider session with a temporary directory.
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
repo_url: Optional URL to a git repository to clone. If provided,
|
| 36 |
+
the repository will be cloned instead of initializing an empty repo.
|
| 37 |
+
|
| 38 |
+
Returns:
|
| 39 |
+
The path to the temporary directory.
|
| 40 |
+
"""
|
| 41 |
+
self.temp_dir = tempfile.mkdtemp(prefix=settings.TEMP_DIR_PREFIX)
|
| 42 |
+
|
| 43 |
+
if repo_url:
|
| 44 |
+
if not self._is_valid_git_url(repo_url):
|
| 45 |
+
raise Exception(f"Invalid git repository URL format: {repo_url}")
|
| 46 |
+
|
| 47 |
+
git_process = await asyncio.create_subprocess_exec(
|
| 48 |
+
"git",
|
| 49 |
+
"clone",
|
| 50 |
+
repo_url,
|
| 51 |
+
".",
|
| 52 |
+
cwd=self.temp_dir,
|
| 53 |
+
stdout=asyncio.subprocess.PIPE,
|
| 54 |
+
stderr=asyncio.subprocess.PIPE,
|
| 55 |
+
)
|
| 56 |
+
stdout, stderr = await git_process.communicate()
|
| 57 |
+
|
| 58 |
+
if git_process.returncode != 0:
|
| 59 |
+
error_msg = (
|
| 60 |
+
stderr.decode() if stderr else "Unknown error during git clone"
|
| 61 |
+
)
|
| 62 |
+
raise Exception(f"Failed to clone repository: {error_msg}")
|
| 63 |
+
|
| 64 |
+
print(f"Successfully cloned repository: {repo_url}")
|
| 65 |
+
else:
|
| 66 |
+
git_process = await asyncio.create_subprocess_exec(
|
| 67 |
+
"git",
|
| 68 |
+
"init",
|
| 69 |
+
cwd=self.temp_dir,
|
| 70 |
+
stdout=asyncio.subprocess.PIPE,
|
| 71 |
+
stderr=asyncio.subprocess.PIPE,
|
| 72 |
+
)
|
| 73 |
+
stdout, stderr = await git_process.communicate()
|
| 74 |
+
|
| 75 |
+
if git_process.returncode != 0:
|
| 76 |
+
error_msg = (
|
| 77 |
+
stderr.decode() if stderr else "Unknown error during git init"
|
| 78 |
+
)
|
| 79 |
+
raise Exception(f"Failed to initialize git repository: {error_msg}")
|
| 80 |
+
|
| 81 |
+
return self.temp_dir
|
| 82 |
+
|
| 83 |
+
def _is_valid_git_url(self, url: str) -> bool:
|
| 84 |
+
"""Validate if the string looks like a git URL."""
|
| 85 |
+
patterns = [
|
| 86 |
+
# SSH format
|
| 87 |
+
r"^git@[a-zA-Z0-9\-\.]+:[a-zA-Z0-9\-\.\/]+\.git$",
|
| 88 |
+
# HTTPS format
|
| 89 |
+
r"^https?:\/\/[a-zA-Z0-9\-\.]+\/[a-zA-Z0-9\-\.\/]+\.git$",
|
| 90 |
+
# Alternative HTTPS format without .git suffix
|
| 91 |
+
r"^https?:\/\/[a-zA-Z0-9\-\.]+\/[a-zA-Z0-9\-\.\/]+$",
|
| 92 |
+
# Filesystem path
|
| 93 |
+
r"^\/[a-zA-Z0-9\-\.\/]+\.git$",
|
| 94 |
+
r"^\/[a-zA-Z0-9\-\.\/]+$",
|
| 95 |
+
r"^[a-zA-Z]:\\[a-zA-Z0-9\-\.\\]+\.git$",
|
| 96 |
+
r"^[a-zA-Z]:\\[a-zA-Z0-9\-\.\\]+$",
|
| 97 |
+
]
|
| 98 |
+
|
| 99 |
+
return any(re.match(pattern, url) for pattern in patterns)
|
| 100 |
+
|
| 101 |
+
async def start_aider(self) -> None:
|
| 102 |
+
"""Start the Aider process."""
|
| 103 |
+
env = os.environ.copy()
|
| 104 |
+
env["OPENAI_API_KEY"] = settings.OPENAI_API_KEY
|
| 105 |
+
env["AZURE_API_KEY"] = settings.AZURE_API_KEY
|
| 106 |
+
env["AZURE_API_VERSION"] = settings.AZURE_API_VERSION
|
| 107 |
+
env["AZURE_API_BASE"] = settings.AZURE_API_BASE
|
| 108 |
+
# env["AIDER_MODEL_METADATA_FILE"] = settings.AIDER_MODEL_METADATA_FILE
|
| 109 |
+
# env["AIDER_GITIGNORE"] = settings.AIDER_GITIGNORE
|
| 110 |
+
|
| 111 |
+
self.aider_process = await asyncio.create_subprocess_exec(
|
| 112 |
+
# "aider", "--model", settings.AIDER_MODEL,
|
| 113 |
+
"aider",
|
| 114 |
+
"--model",
|
| 115 |
+
"azure/bcsai-gpt4o",
|
| 116 |
+
"--no-gitignore",
|
| 117 |
+
"--no-show-model-warnings",
|
| 118 |
+
"--no-suggest-shell-commands",
|
| 119 |
+
"--no-check-update",
|
| 120 |
+
stdin=asyncio.subprocess.PIPE,
|
| 121 |
+
stdout=asyncio.subprocess.PIPE,
|
| 122 |
+
stderr=asyncio.subprocess.PIPE,
|
| 123 |
+
cwd=self.temp_dir,
|
| 124 |
+
env=env,
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
async def send_command(self, command: str) -> None:
|
| 128 |
+
"""Send a command to the Aider process."""
|
| 129 |
+
if not self.aider_process or not self.aider_process.stdin:
|
| 130 |
+
raise Exception("Aider process is not running or stdin is not available")
|
| 131 |
+
|
| 132 |
+
try:
|
| 133 |
+
if not command.endswith("\n"):
|
| 134 |
+
command += "\n"
|
| 135 |
+
|
| 136 |
+
self.aider_process.stdin.write(command.encode())
|
| 137 |
+
await self.aider_process.stdin.drain()
|
| 138 |
+
except Exception as e:
|
| 139 |
+
raise Exception(f"Failed to send command to Aider: {str(e)}")
|
| 140 |
+
|
| 141 |
+
async def process_output(self, websocket: WebSocket) -> None:
|
| 142 |
+
"""Process and send Aider output to the websocket."""
|
| 143 |
+
if not self.aider_process:
|
| 144 |
+
return
|
| 145 |
+
|
| 146 |
+
try:
|
| 147 |
+
while True:
|
| 148 |
+
line = await self.aider_process.stdout.readline()
|
| 149 |
+
if not line:
|
| 150 |
+
break
|
| 151 |
+
|
| 152 |
+
line_str = line.decode().strip()
|
| 153 |
+
if not line_str:
|
| 154 |
+
continue
|
| 155 |
+
|
| 156 |
+
blocks = self.output_parser.parse_output(line_str)
|
| 157 |
+
|
| 158 |
+
for block in blocks:
|
| 159 |
+
if block.is_code_block:
|
| 160 |
+
html_output = self.converter.convert(block.content, full=False)
|
| 161 |
+
await websocket.send_json(
|
| 162 |
+
{"type": "code_block", "content": html_output}
|
| 163 |
+
)
|
| 164 |
+
elif block.is_prompt:
|
| 165 |
+
await websocket.send_json(
|
| 166 |
+
{"type": "prompt", "content": block.content}
|
| 167 |
+
)
|
| 168 |
+
elif block.is_separator:
|
| 169 |
+
await websocket.send_json(
|
| 170 |
+
{"type": "separator", "content": block.content}
|
| 171 |
+
)
|
| 172 |
+
elif block.is_token_info:
|
| 173 |
+
await websocket.send_json(
|
| 174 |
+
{"type": "token_info", "content": block.content}
|
| 175 |
+
)
|
| 176 |
+
else:
|
| 177 |
+
html_output = self.converter.convert(block.content, full=False)
|
| 178 |
+
await websocket.send_json(
|
| 179 |
+
{"type": "output", "content": html_output}
|
| 180 |
+
)
|
| 181 |
+
except Exception as e:
|
| 182 |
+
await websocket.send_json(
|
| 183 |
+
{"type": "error", "content": f"Error processing output: {str(e)}"}
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
async def create_download_zip(self) -> Optional[str]:
|
| 187 |
+
"""Create a zip file of the temporary directory contents, excluding specified patterns.
|
| 188 |
+
|
| 189 |
+
Returns:
|
| 190 |
+
The path to the created zip file, or None if the temporary directory doesn't exist.
|
| 191 |
+
"""
|
| 192 |
+
if not self.temp_dir or not os.path.exists(self.temp_dir):
|
| 193 |
+
return None
|
| 194 |
+
|
| 195 |
+
zip_path = tempfile.mktemp(suffix=".zip")
|
| 196 |
+
|
| 197 |
+
try:
|
| 198 |
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
|
| 199 |
+
for root, dirs, files in os.walk(self.temp_dir):
|
| 200 |
+
dirs[:] = [
|
| 201 |
+
d
|
| 202 |
+
for d in dirs
|
| 203 |
+
if not any(
|
| 204 |
+
fnmatch.fnmatch(d, pattern)
|
| 205 |
+
for pattern in settings.EXCLUDE_PATTERNS
|
| 206 |
+
)
|
| 207 |
+
]
|
| 208 |
+
|
| 209 |
+
for file in files:
|
| 210 |
+
file_path = os.path.join(root, file)
|
| 211 |
+
if any(
|
| 212 |
+
fnmatch.fnmatch(file, pattern)
|
| 213 |
+
for pattern in settings.EXCLUDE_PATTERNS
|
| 214 |
+
):
|
| 215 |
+
continue
|
| 216 |
+
|
| 217 |
+
arcname = os.path.relpath(file_path, self.temp_dir)
|
| 218 |
+
zipf.write(file_path, arcname)
|
| 219 |
+
|
| 220 |
+
return zip_path
|
| 221 |
+
except Exception as e:
|
| 222 |
+
print(f"Error creating zip file: {str(e)}")
|
| 223 |
+
if os.path.exists(zip_path):
|
| 224 |
+
os.remove(zip_path)
|
| 225 |
+
return None
|
| 226 |
+
|
| 227 |
+
async def cleanup(self) -> None:
|
| 228 |
+
"""Clean up resources."""
|
| 229 |
+
if self.aider_process and self.aider_process.returncode is None:
|
| 230 |
+
try:
|
| 231 |
+
self.aider_process.terminate()
|
| 232 |
+
await self.aider_process.wait()
|
| 233 |
+
except Exception:
|
| 234 |
+
pass
|
| 235 |
+
|
| 236 |
+
if self.running_process and self.running_process.returncode is None:
|
| 237 |
+
try:
|
| 238 |
+
self.running_process.send_signal(signal.SIGINT)
|
| 239 |
+
await self.running_process.wait()
|
| 240 |
+
except Exception:
|
| 241 |
+
pass
|
| 242 |
+
|
| 243 |
+
if self.temp_dir:
|
| 244 |
+
try:
|
| 245 |
+
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
| 246 |
+
except Exception:
|
| 247 |
+
pass
|
| 248 |
+
self.temp_dir = None
|
| 249 |
+
|
| 250 |
+
async def list_files(self) -> list:
|
| 251 |
+
"""List files in the temporary directory."""
|
| 252 |
+
if not self.temp_dir:
|
| 253 |
+
return []
|
| 254 |
+
|
| 255 |
+
files = []
|
| 256 |
+
for root, _, filenames in os.walk(self.temp_dir):
|
| 257 |
+
for filename in filenames:
|
| 258 |
+
files.append(
|
| 259 |
+
os.path.relpath(os.path.join(root, filename), self.temp_dir)
|
| 260 |
+
)
|
| 261 |
+
return files
|
| 262 |
+
|
| 263 |
+
async def read_file(self, filename: str) -> str:
|
| 264 |
+
"""Read a file from the temporary directory."""
|
| 265 |
+
if not self.temp_dir:
|
| 266 |
+
return "File not found"
|
| 267 |
+
|
| 268 |
+
file_path = os.path.join(self.temp_dir, filename)
|
| 269 |
+
if os.path.exists(file_path):
|
| 270 |
+
with open(file_path, "r") as f:
|
| 271 |
+
return f.read()
|
| 272 |
+
return "File not found"
|
app/services/session_manager.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Session manager for AiderService."""
|
| 2 |
+
|
| 3 |
+
import uuid
|
| 4 |
+
from typing import Dict, Optional
|
| 5 |
+
|
| 6 |
+
from app.services.aider_service import AiderService
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class SessionManager:
|
| 10 |
+
"""Session manager for AiderService."""
|
| 11 |
+
|
| 12 |
+
def __init__(self) -> None:
|
| 13 |
+
"""Initialize the session manager."""
|
| 14 |
+
self.sessions: Dict[str, AiderService] = {}
|
| 15 |
+
|
| 16 |
+
async def create_session(self, repo_url: Optional[str] = None) -> str:
|
| 17 |
+
"""Create a new session and return its ID.
|
| 18 |
+
|
| 19 |
+
Args:
|
| 20 |
+
repo_url: Optional URL to a git repository to clone. If provided,
|
| 21 |
+
the repository will be cloned instead of initializing an empty repo.
|
| 22 |
+
|
| 23 |
+
Returns:
|
| 24 |
+
The session ID as a string.
|
| 25 |
+
"""
|
| 26 |
+
session_id = str(uuid.uuid4())
|
| 27 |
+
aider_service = AiderService()
|
| 28 |
+
await aider_service.initialize(repo_url=repo_url)
|
| 29 |
+
await aider_service.start_aider()
|
| 30 |
+
self.sessions[session_id] = aider_service
|
| 31 |
+
return session_id
|
| 32 |
+
|
| 33 |
+
def get_session(self, session_id: str) -> Optional[AiderService]:
|
| 34 |
+
"""Get an existing session by ID."""
|
| 35 |
+
return self.sessions.get(session_id)
|
| 36 |
+
|
| 37 |
+
async def cleanup_session(self, session_id: str) -> None:
|
| 38 |
+
"""Clean up a session and remove it from the manager."""
|
| 39 |
+
if session_id in self.sessions:
|
| 40 |
+
await self.sessions[session_id].cleanup()
|
| 41 |
+
del self.sessions[session_id]
|
| 42 |
+
|
| 43 |
+
async def cleanup_all_sessions(self) -> None:
|
| 44 |
+
"""Clean up all sessions."""
|
| 45 |
+
for session_id in list(self.sessions.keys()):
|
| 46 |
+
await self.cleanup_session(session_id)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
session_manager = SessionManager()
|
app/utils/output_parser.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module to parse the output of the Aider model."""
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from typing import List
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@dataclass
|
| 9 |
+
class AiderOutputBlock:
|
| 10 |
+
"""A single block of output from the Aider model."""
|
| 11 |
+
|
| 12 |
+
content: str
|
| 13 |
+
type: str # 'text', 'code', 'prompt', 'separator', 'token_info'
|
| 14 |
+
is_code_block: bool = False
|
| 15 |
+
is_prompt: bool = False
|
| 16 |
+
is_separator: bool = False
|
| 17 |
+
is_token_info: bool = False
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class AiderOutputParser:
|
| 21 |
+
"""Output parser for Aider model."""
|
| 22 |
+
|
| 23 |
+
def __init__(self) -> None:
|
| 24 |
+
"""Initialize the output parser."""
|
| 25 |
+
self.code_block_pattern = re.compile(
|
| 26 |
+
r"<<<<<<< SEARCH\n(.*?)\n=======\n(.*?)\n>>>>>>> REPLACE", re.DOTALL
|
| 27 |
+
)
|
| 28 |
+
self.prompt_pattern = re.compile(r"^> (.*)$", re.MULTILINE)
|
| 29 |
+
self.separator_pattern = re.compile(r"^─+$", re.MULTILINE)
|
| 30 |
+
self.token_info_pattern = re.compile(
|
| 31 |
+
r"Tokens: (.*?) sent, (.*?) received\. Cost: (.*?) message, (.*?) session\."
|
| 32 |
+
)
|
| 33 |
+
self.aider_pattern = re.compile(r"\baider\b", re.IGNORECASE)
|
| 34 |
+
|
| 35 |
+
def parse_output(self, output: str) -> List[AiderOutputBlock]:
|
| 36 |
+
"""Parse the output from the Aider model into blocks."""
|
| 37 |
+
blocks = []
|
| 38 |
+
current_block = []
|
| 39 |
+
|
| 40 |
+
for line in output.split("\n"):
|
| 41 |
+
|
| 42 |
+
line = line.rstrip("\r")
|
| 43 |
+
|
| 44 |
+
line = re.sub(self.aider_pattern, "Dev Assist", line)
|
| 45 |
+
|
| 46 |
+
if not line.strip():
|
| 47 |
+
if current_block:
|
| 48 |
+
blocks.append(self._create_text_block("".join(current_block)))
|
| 49 |
+
current_block = []
|
| 50 |
+
continue
|
| 51 |
+
|
| 52 |
+
if "<<<<<<< SEARCH" in line:
|
| 53 |
+
if current_block:
|
| 54 |
+
blocks.append(self._create_text_block("".join(current_block)))
|
| 55 |
+
current_block = []
|
| 56 |
+
blocks.append(self._create_code_block(line))
|
| 57 |
+
continue
|
| 58 |
+
|
| 59 |
+
if line.startswith("> "):
|
| 60 |
+
if current_block:
|
| 61 |
+
blocks.append(self._create_text_block("".join(current_block)))
|
| 62 |
+
current_block = []
|
| 63 |
+
prompt_content = line[2:].strip()
|
| 64 |
+
blocks.append(self._create_prompt_block(prompt_content))
|
| 65 |
+
continue
|
| 66 |
+
|
| 67 |
+
if re.match(self.separator_pattern, line):
|
| 68 |
+
if current_block:
|
| 69 |
+
blocks.append(self._create_text_block("".join(current_block)))
|
| 70 |
+
current_block = []
|
| 71 |
+
blocks.append(self._create_separator_block(line))
|
| 72 |
+
continue
|
| 73 |
+
|
| 74 |
+
if re.match(self.token_info_pattern, line):
|
| 75 |
+
if current_block:
|
| 76 |
+
blocks.append(self._create_text_block("".join(current_block)))
|
| 77 |
+
current_block = []
|
| 78 |
+
blocks.append(self._create_token_info_block(line))
|
| 79 |
+
continue
|
| 80 |
+
|
| 81 |
+
current_block.append(line + "\n")
|
| 82 |
+
|
| 83 |
+
if current_block:
|
| 84 |
+
blocks.append(self._create_text_block("".join(current_block)))
|
| 85 |
+
|
| 86 |
+
return blocks
|
| 87 |
+
|
| 88 |
+
def _create_text_block(self, content: str) -> AiderOutputBlock:
|
| 89 |
+
return AiderOutputBlock(content=content, type="text")
|
| 90 |
+
|
| 91 |
+
def _create_code_block(self, content: str) -> AiderOutputBlock:
|
| 92 |
+
return AiderOutputBlock(content=content, type="code", is_code_block=True)
|
| 93 |
+
|
| 94 |
+
def _create_prompt_block(self, content: str) -> AiderOutputBlock:
|
| 95 |
+
return AiderOutputBlock(content=content, type="prompt", is_prompt=True)
|
| 96 |
+
|
| 97 |
+
def _create_separator_block(self, content: str) -> AiderOutputBlock:
|
| 98 |
+
return AiderOutputBlock(content=content, type="separator", is_separator=True)
|
| 99 |
+
|
| 100 |
+
def _create_token_info_block(self, content: str) -> AiderOutputBlock:
|
| 101 |
+
return AiderOutputBlock(content=content, type="token_info", is_token_info=True)
|
client.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import streamlit.components.v1 as components
|
| 3 |
+
|
| 4 |
+
st.title("Aider WebSocket Client")
|
| 5 |
+
st.write(
|
| 6 |
+
"Interact with the Aider WebSocket server. Send commands, run files, and terminate running processes."
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
html_code = """
|
| 10 |
+
<div>
|
| 11 |
+
<div id="messages" style="height: 300px; overflow-y: scroll; border: 1px solid #ccc; padding: 10px;"></div>
|
| 12 |
+
<input id="repo_url" type="text" placeholder="Repository URL (optional)" style="margin-top: 10px; width: 300px;">
|
| 13 |
+
<button onclick="connectToAider()" style="margin-left: 10px;">Connect to Aider</button>
|
| 14 |
+
<input id="aider_input" type="text" placeholder="Send to Aider" style="margin-top: 10px; width: 300px;">
|
| 15 |
+
<button onclick="sendToAider()" style="margin-left: 10px;">Send to Aider</button>
|
| 16 |
+
<button onclick="listFiles()" style="margin-left: 10px;">List Files</button>
|
| 17 |
+
<input id="filename_input" type="text" placeholder="Filename to run/read" style="margin-top: 10px; width: 300px;">
|
| 18 |
+
<button onclick="runFile()" style="margin-left: 10px;">Run File</button>
|
| 19 |
+
<button onclick="terminateProcess()" style="margin-left: 10px;">Terminate Process</button>
|
| 20 |
+
<button onclick="readFile()" style="margin-left: 10px;">Read File</button>
|
| 21 |
+
<button onclick="downloadFiles()" style="margin-left: 10px;">Download Files</button>
|
| 22 |
+
</div>
|
| 23 |
+
<script>
|
| 24 |
+
let ws;
|
| 25 |
+
|
| 26 |
+
function connectToAider() {
|
| 27 |
+
const repoUrl = document.getElementById('repo_url').value;
|
| 28 |
+
|
| 29 |
+
// Close existing connection if any
|
| 30 |
+
if (ws) {
|
| 31 |
+
ws.close();
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// Create a new WebSocket connection
|
| 35 |
+
ws = new WebSocket('ws://localhost:8000/ws');
|
| 36 |
+
|
| 37 |
+
ws.onopen = function() {
|
| 38 |
+
// Send initial connection parameters
|
| 39 |
+
ws.send(JSON.stringify({
|
| 40 |
+
repo_url: repoUrl || null
|
| 41 |
+
}));
|
| 42 |
+
|
| 43 |
+
const messagesDiv = document.getElementById('messages');
|
| 44 |
+
messagesDiv.innerHTML += '<div style="color: green;">Connected to Aider' + (repoUrl ? ' with repository: ' + repoUrl : '') + '</div><br>';
|
| 45 |
+
|
| 46 |
+
// Enable the input field
|
| 47 |
+
document.getElementById('aider_input').disabled = false;
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
ws.onmessage = function(event) {
|
| 51 |
+
// Check if the message is binary (zip file)
|
| 52 |
+
if (event.data instanceof Blob) {
|
| 53 |
+
// Create a download link for the zip file
|
| 54 |
+
const url = window.URL.createObjectURL(event.data);
|
| 55 |
+
const a = document.createElement('a');
|
| 56 |
+
a.href = url;
|
| 57 |
+
a.download = 'aider_files.zip';
|
| 58 |
+
document.body.appendChild(a);
|
| 59 |
+
a.click();
|
| 60 |
+
window.URL.revokeObjectURL(url);
|
| 61 |
+
document.body.removeChild(a);
|
| 62 |
+
return;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
const data = JSON.parse(event.data);
|
| 66 |
+
const messagesDiv = document.getElementById('messages');
|
| 67 |
+
if (data.type === 'output') {
|
| 68 |
+
messagesDiv.innerHTML += data.content + '<br>';
|
| 69 |
+
} else if (data.type === 'files') {
|
| 70 |
+
messagesDiv.innerHTML += 'Files: ' + data.content.join(', ') + '<br>';
|
| 71 |
+
} else if (data.type === 'file_content') {
|
| 72 |
+
messagesDiv.innerHTML += 'File content:<br>' + data.content + '<br>';
|
| 73 |
+
} else if (data.type === 'error') {
|
| 74 |
+
messagesDiv.innerHTML += '<div style="color: red;">Error: ' + data.content + '</div><br>';
|
| 75 |
+
} else if (data.type === 'status') {
|
| 76 |
+
messagesDiv.innerHTML += '<div style="color: blue;">Status: ' + data.content + '</div><br>';
|
| 77 |
+
}
|
| 78 |
+
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
ws.onerror = function(error) {
|
| 82 |
+
const messagesDiv = document.getElementById('messages');
|
| 83 |
+
messagesDiv.innerHTML += '<div style="color: red;">WebSocket Error: ' + error.message + '</div><br>';
|
| 84 |
+
document.getElementById('aider_input').disabled = true;
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
ws.onclose = function(event) {
|
| 88 |
+
const messagesDiv = document.getElementById('messages');
|
| 89 |
+
if (event.wasClean) {
|
| 90 |
+
messagesDiv.innerHTML += '<div style="color: blue;">Connection closed cleanly, code=' + event.code + ' reason=' + event.reason + '</div><br>';
|
| 91 |
+
} else {
|
| 92 |
+
messagesDiv.innerHTML += '<div style="color: red;">Connection died unexpectedly</div><br>';
|
| 93 |
+
}
|
| 94 |
+
document.getElementById('aider_input').disabled = true;
|
| 95 |
+
};
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
function sendToAider() {
|
| 99 |
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
| 100 |
+
alert('Please connect to Aider first');
|
| 101 |
+
return;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
const input = document.getElementById('aider_input').value;
|
| 105 |
+
if (input) {
|
| 106 |
+
ws.send(JSON.stringify({command: 'send_to_aider', message: input}));
|
| 107 |
+
document.getElementById('aider_input').value = '';
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
function listFiles() {
|
| 112 |
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
| 113 |
+
alert('Please connect to Aider first');
|
| 114 |
+
return;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
ws.send(JSON.stringify({command: 'list_files'}));
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
function readFile() {
|
| 121 |
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
| 122 |
+
alert('Please connect to Aider first');
|
| 123 |
+
return;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
const filename = document.getElementById('filename_input').value;
|
| 127 |
+
if (filename) {
|
| 128 |
+
ws.send(JSON.stringify({command: 'read_file', filename: filename}));
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
function runFile() {
|
| 133 |
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
| 134 |
+
alert('Please connect to Aider first');
|
| 135 |
+
return;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
const filename = document.getElementById('filename_input').value;
|
| 139 |
+
if (filename) {
|
| 140 |
+
ws.send(JSON.stringify({command: 'run_file', filename: filename}));
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
function terminateProcess() {
|
| 145 |
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
| 146 |
+
alert('Please connect to Aider first');
|
| 147 |
+
return;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
ws.send(JSON.stringify({command: 'terminate_process'}));
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
function downloadFiles() {
|
| 154 |
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
| 155 |
+
alert('Please connect to Aider first');
|
| 156 |
+
return;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
ws.send(JSON.stringify({command: 'download'}));
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
// Show connection button by default
|
| 163 |
+
window.onload = function() {
|
| 164 |
+
document.getElementById('aider_input').disabled = true;
|
| 165 |
+
};
|
| 166 |
+
</script>
|
| 167 |
+
"""
|
| 168 |
+
|
| 169 |
+
components.html(html_code, height=500)
|
mypy.ini
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[mypy]
|
| 2 |
+
python_version = 3.12
|
| 3 |
+
ignore_missing_imports = True
|
| 4 |
+
|
| 5 |
+
[mypy-requests.*]
|
| 6 |
+
ignore_missing_imports = True
|
| 7 |
+
# Paths to exclude from checking
|
| 8 |
+
# exclude = venv|__jac_gen__|tests|stubs|support|vendor|examples/reference|setup.py
|
| 9 |
+
|
| 10 |
+
# Treating type checking issues as errors
|
| 11 |
+
# strict = True
|
| 12 |
+
|
| 13 |
+
# Allow redefinition of functions with different types
|
| 14 |
+
# allow_redefinition = True
|
| 15 |
+
|
| 16 |
+
# Ignore missing imports from certain modules
|
| 17 |
+
# ignore_missing_imports = True
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.68.0
|
| 2 |
+
uvicorn>=0.15.0
|
| 3 |
+
python-dotenv>=0.19.0
|
| 4 |
+
ansi2html>=1.8.0
|
| 5 |
+
pydantic>=2.0.0
|
| 6 |
+
pydantic-settings>=2.0.0
|
| 7 |
+
aider-install
|
| 8 |
+
shutil
|