ethanrom commited on
Commit
acd245a
·
verified ·
1 Parent(s): 690df69

Upload 17 files

Browse files
.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