pyaesonegtckglay-dotcom commited on
Commit
33302b5
·
1 Parent(s): 36408aa

🚀 Upgrade to Devin Agent Platform v2.0

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +0 -34
  2. Dockerfile +34 -127
  3. README.md +52 -18
  4. __pycache__/main.cpython-312.pyc +0 -0
  5. ngpasswd → api/__init__.py +0 -0
  6. api/__pycache__/__init__.cpython-312.pyc +0 -0
  7. api/__pycache__/websocket_manager.cpython-312.pyc +0 -0
  8. root/etc/s6-overlay/s6-rc.d/init-config-end/dependencies.d/init-openvscode-server → api/routes/__init__.py +0 -0
  9. api/routes/__pycache__/__init__.cpython-312.pyc +0 -0
  10. api/routes/__pycache__/chat.cpython-312.pyc +0 -0
  11. api/routes/__pycache__/github.cpython-312.pyc +0 -0
  12. api/routes/__pycache__/health.cpython-312.pyc +0 -0
  13. api/routes/__pycache__/memory.cpython-312.pyc +0 -0
  14. api/routes/__pycache__/tasks.cpython-312.pyc +0 -0
  15. api/routes/chat.py +214 -0
  16. api/routes/github.py +336 -0
  17. api/routes/health.py +53 -0
  18. api/routes/memory.py +50 -0
  19. api/routes/tasks.py +167 -0
  20. api/websocket_manager.py +134 -0
  21. api_server.py +0 -44
  22. auth.py +0 -41
  23. core/__init__.py +0 -1
  24. core/__pycache__/__init__.cpython-312.pyc +0 -0
  25. core/__pycache__/agent.cpython-312.pyc +0 -0
  26. core/__pycache__/models.cpython-312.pyc +0 -0
  27. core/__pycache__/task_engine.cpython-312.pyc +0 -0
  28. core/agent.py +392 -0
  29. core/database.py +0 -132
  30. core/github_engine.py +0 -80
  31. core/llm_router.py +0 -37
  32. core/memory.py +0 -44
  33. core/models.py +213 -0
  34. core/orchestrator.py +0 -96
  35. core/task_engine.py +241 -0
  36. ecosystem.config.cjs +20 -0
  37. root/etc/s6-overlay/s6-rc.d/init-openvscode-server/dependencies.d/init-config → github/__init__.py +0 -0
  38. main.py +180 -0
  39. root/etc/s6-overlay/s6-rc.d/svc-openvscode-server/dependencies.d/init-services → memory/__init__.py +0 -0
  40. memory/__pycache__/__init__.cpython-312.pyc +0 -0
  41. memory/__pycache__/db.cpython-312.pyc +0 -0
  42. memory/db.py +271 -0
  43. nginx.conf +0 -129
  44. on_startup.sh +0 -31
  45. packages.txt +0 -1
  46. requirements.txt +28 -12
  47. root/etc/s6-overlay/s6-rc.d/init-openvscode-server/run +0 -35
  48. root/etc/s6-overlay/s6-rc.d/init-openvscode-server/type +0 -1
  49. root/etc/s6-overlay/s6-rc.d/init-openvscode-server/up +0 -1
  50. root/etc/s6-overlay/s6-rc.d/svc-openvscode-server/notification-fd +0 -1
.gitattributes DELETED
@@ -1,34 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tflite filter=lfs diff=lfs merge=lfs -text
29
- *.tgz filter=lfs diff=lfs merge=lfs -text
30
- *.wasm filter=lfs diff=lfs merge=lfs -text
31
- *.xz filter=lfs diff=lfs merge=lfs -text
32
- *.zip filter=lfs diff=lfs merge=lfs -text
33
- *.zst filter=lfs diff=lfs merge=lfs -text
34
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile CHANGED
@@ -1,141 +1,48 @@
1
- FROM nvidia/cuda:11.3.1-base-ubuntu20.04
2
 
3
- ENV DEBIAN_FRONTEND=noninteractive \
4
- TZ=Aisa/Shanghai \
5
- LC_CTYPE=C.UTF-8 \
6
- LANG=C.UTF-8
7
 
8
- # Remove any third-party apt sources to avoid issues with expiring keys.
9
- # Install some basic utilities
10
- RUN rm -f /etc/apt/sources.list.d/*.list && \
11
- apt-get update && apt-get install -y \
12
- curl \
13
- ca-certificates \
14
- sudo \
15
- git \
16
- git-lfs \
17
- zip \
18
- unzip \
19
- htop \
20
- bzip2 \
21
- libx11-6 \
22
- nginx \
23
- vim \
24
- lsof \
25
- telnet \
26
- wget \
27
- build-essential \
28
- libsndfile-dev \
29
- software-properties-common \
30
- && rm -rf /var/lib/apt/lists/*
31
-
32
- ARG BUILD_DATE
33
- ARG VERSION
34
- ARG CODE_RELEASE
35
- RUN \
36
- echo "**** install openvscode-server runtime dependencies ****" && \
37
- apt-get update && \
38
- apt-get install -y \
39
- jq \
40
- libatomic1 \
41
- nano \
42
- net-tools \
43
- netcat && \
44
- echo "**** install openvscode-server ****" && \
45
- if [ -z ${CODE_RELEASE+x} ]; then \
46
- CODE_RELEASE=$(curl -sX GET "https://api.github.com/repos/gitpod-io/openvscode-server/releases/latest" \
47
- | awk '/tag_name/{print $4;exit}' FS='[""]' \
48
- | sed 's|^openvscode-server-v||'); \
49
- fi && \
50
- mkdir -p /app/openvscode-server && \
51
- curl -o \
52
- /tmp/openvscode-server.tar.gz -L \
53
- "https://github.com/gitpod-io/openvscode-server/releases/download/openvscode-server-v${CODE_RELEASE}/openvscode-server-v${CODE_RELEASE}-linux-x64.tar.gz" && \
54
- tar xf \
55
- /tmp/openvscode-server.tar.gz -C \
56
- /app/openvscode-server/ --strip-components=1 && \
57
- echo "**** clean up ****" && \
58
- apt-get clean && \
59
- rm -rf \
60
- /tmp/* \
61
- /var/lib/apt/lists/* \
62
- /var/tmp/*
63
- COPY root/ /
64
-
65
- RUN add-apt-repository ppa:flexiondotorg/nvtop && \
66
- apt-get upgrade -y && \
67
- apt-get install -y --no-install-recommends nvtop
68
-
69
- RUN curl -sL https://deb.nodesource.com/setup_18.x | bash - && \
70
- apt-get install -y nodejs && \
71
- npm install -g configurable-http-proxy
72
-
73
- # Create a working directory
74
  WORKDIR /app
75
 
76
- # Create a non-root user and switch to it
77
- RUN adduser --disabled-password --gecos '' --shell /bin/bash user \
78
- && chown -R user:user /app
79
- RUN echo "user ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/90-user
80
- USER user
81
-
82
- # All users can use /home/user as their home directory
83
- ENV HOME=/home/user
84
- RUN mkdir $HOME/.cache $HOME/.config \
85
- && chmod -R 777 $HOME
86
-
87
- # Set up the Conda environment
88
- ENV CONDA_AUTO_UPDATE_CONDA=false \
89
- PATH=$HOME/miniconda/bin:$PATH
90
- RUN curl -sLo ~/miniconda.sh https://repo.continuum.io/miniconda/Miniconda3-py310_23.5.2-0-Linux-x86_64.sh \
91
- && chmod +x ~/miniconda.sh \
92
- && ~/miniconda.sh -b -p ~/miniconda \
93
- && rm ~/miniconda.sh \
94
- && conda clean -ya
95
-
96
- WORKDIR $HOME/app
97
-
98
- #######################################
99
- # Start root user section
100
- #######################################
101
-
102
- USER root
103
-
104
- # User Debian packages
105
- ## Security warning : Potential user code executed as root (build time)
106
- RUN --mount=target=/root/packages.txt,source=packages.txt \
107
- apt-get update && \
108
- xargs -r -a /root/packages.txt apt-get install -y --no-install-recommends \
109
  && rm -rf /var/lib/apt/lists/*
110
 
111
- RUN --mount=target=/root/on_startup.sh,source=on_startup.sh,readwrite \
112
- bash /root/on_startup.sh
113
-
114
- #######################################
115
- # End root user section
116
- #######################################
117
 
118
- USER user
 
119
 
120
- # Python packages
121
- RUN --mount=target=requirements.txt,source=requirements.txt \
122
- pip install --no-cache-dir --upgrade -r requirements.txt
123
 
124
- # Copy the current directory contents into the container at $HOME/app setting the owner to the user
125
- COPY --chown=user . $HOME/app
 
126
 
127
- WORKDIR $HOME/app
128
 
129
- RUN chmod +x start_server.sh
130
 
131
- ENV PYTHONUNBUFFERED=1 \
132
- GRADIO_ALLOW_FLAGGING=never \
133
- GRADIO_NUM_PORTS=1 \
134
- GRADIO_SERVER_NAME=0.0.0.0 \
135
- GRADIO_THEME=huggingface \
136
- SYSTEM=spaces \
137
- SHELL=/bin/bash
138
 
139
- EXPOSE 7860 3000
 
 
140
 
141
- CMD ["./start_server.sh"]
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
 
3
+ # HuggingFace Spaces Dockerfile
4
+ # Compatible with free CPU tier
 
 
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  WORKDIR /app
7
 
8
+ # System deps
9
+ RUN apt-get update && apt-get install -y \
10
+ git curl build-essential \
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  && rm -rf /var/lib/apt/lists/*
12
 
13
+ # Python deps
14
+ COPY requirements.txt .
15
+ RUN pip install --no-cache-dir --upgrade pip && \
16
+ pip install --no-cache-dir -r requirements.txt
 
 
17
 
18
+ # App code
19
+ COPY . .
20
 
21
+ # Setup dirs
22
+ RUN mkdir -p /tmp/workspace /tmp/repos /tmp/devin_data
 
23
 
24
+ # HF runs as uid 1000
25
+ RUN useradd -m -u 1000 user 2>/dev/null || true
26
+ RUN chown -R 1000:1000 /app /tmp/workspace /tmp/repos /tmp/devin_data
27
 
28
+ USER 1000
29
 
30
+ EXPOSE 7860
31
 
32
+ ENV PORT=7860
33
+ ENV HOST=0.0.0.0
34
+ ENV DB_PATH=/tmp/devin_agent.db
35
+ ENV PYTHONUNBUFFERED=1
36
+ ENV PYTHONDONTWRITEBYTECODE=1
 
 
37
 
38
+ # Health check
39
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
40
+ CMD curl -f http://localhost:7860/api/v1/health || exit 1
41
 
42
+ CMD ["uvicorn", "main:app", \
43
+ "--host", "0.0.0.0", \
44
+ "--port", "7860", \
45
+ "--workers", "1", \
46
+ "--loop", "asyncio", \
47
+ "--timeout-keep-alive", "75", \
48
+ "--log-level", "info"]
README.md CHANGED
@@ -1,24 +1,58 @@
1
  ---
2
- title: Visual Studio Code
3
- emoji: 💻🐳
4
- colorFrom: red
5
- colorTo: blue
6
  sdk: docker
7
- pinned: false
8
- tags:
9
- - vscode
10
- duplicated_from: SpacesExamples/vscode
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
14
- # Autonomous Coding System (Upgraded)
15
 
16
- This system has been upgraded with:
17
- 1. **Persistent Memory Engine**: SQLite-backed storage for goals, plans, and execution history.
18
- 2. **Full GitHub Automation**: Complete control over repositories, branches, commits, and PRs.
19
- 3. **Tool Orchestration Brain**: Autonomous planning and tool routing for complex tasks.
20
 
21
- ## New API Endpoints
22
- - `/api/v1/goal`: Submit a high-level goal for autonomous execution.
23
- - `/api/v1/memory/{project_id}`: Retrieve project history.
24
- - `/api/v1/github/*`: Full suite of GitHub automation tools.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Devin Agent Platform
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: docker
7
+ app_port: 7860
8
+ pinned: true
9
+ license: mit
10
+ short_description: Production-grade autonomous AI engineering platform
11
  ---
12
 
13
+ # 🤖 Devin Agent Platform v2.0
 
14
 
15
+ > **Manus/Devin-style Autonomous AI Engineering Platform**
16
+ > Real-time WebSocket streaming · Autonomous GitHub operations · Persistent memory
 
 
17
 
18
+ ## Features
19
+
20
+ - **Real-time WebSocket streaming** — live token-by-token LLM output
21
+ - 🗺️ **Autonomous task planning** goal plan → execute automatically
22
+ - 🧠 **Persistent memory** — SQLite-backed conversation + project memory
23
+ - 🐙 **GitHub automation** — clone, commit, push, PR, issues autonomously
24
+ - 🔁 **Self-healing** — auto-retry with exponential backoff
25
+ - 📡 **SSE fallback** — Server-Sent Events for streaming compatibility
26
+ - 🌐 **REST + WebSocket API** — full-featured backend
27
+
28
+ ## 🔌 API Endpoints
29
+
30
+ | Method | Endpoint | Description |
31
+ |--------|----------|-------------|
32
+ | POST | `/api/v1/tasks/create` | Create autonomous task |
33
+ | GET | `/api/v1/tasks/{id}` | Get task details |
34
+ | POST | `/api/v1/tasks/{id}/cancel` | Cancel task |
35
+ | POST | `/api/v1/tasks/{id}/retry` | Retry failed task |
36
+ | GET | `/api/v1/tasks/{id}/stream` | SSE task stream |
37
+ | POST | `/api/v1/chat` | Chat with agent |
38
+ | POST | `/api/v1/goal` | Submit high-level goal |
39
+ | POST | `/api/v1/plan` | Generate execution plan |
40
+ | WS | `/ws/tasks/{task_id}` | Live task WebSocket |
41
+ | WS | `/ws/logs` | Global log stream |
42
+ | WS | `/ws/chat/{session_id}` | Chat WebSocket |
43
+ | WS | `/ws/agent/status` | Agent status stream |
44
+
45
+ ## 🔑 Environment Variables (HF Secrets)
46
+
47
+ ```
48
+ OPENAI_API_KEY = sk-... (for real AI)
49
+ ANTHROPIC_API_KEY = sk-ant-... (alternative)
50
+ GITHUB_TOKEN = ghp_... (GitHub ops)
51
+ GITHUB_OWNER = your-username (GitHub ops)
52
+ ```
53
+
54
+ ## 🚀 Quick Start
55
+
56
+ Visit `/api/docs` for interactive Swagger UI.
57
+
58
+ **Demo mode** works without any API keys — set `OPENAI_API_KEY` for real AI.
__pycache__/main.cpython-312.pyc ADDED
Binary file (9.42 kB). View file
 
ngpasswd → api/__init__.py RENAMED
File without changes
api/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (143 Bytes). View file
 
api/__pycache__/websocket_manager.cpython-312.pyc ADDED
Binary file (7.67 kB). View file
 
root/etc/s6-overlay/s6-rc.d/init-config-end/dependencies.d/init-openvscode-server → api/routes/__init__.py RENAMED
File without changes
api/routes/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (150 Bytes). View file
 
api/routes/__pycache__/chat.cpython-312.pyc ADDED
Binary file (10.6 kB). View file
 
api/routes/__pycache__/github.cpython-312.pyc ADDED
Binary file (17.8 kB). View file
 
api/routes/__pycache__/health.cpython-312.pyc ADDED
Binary file (2.93 kB). View file
 
api/routes/__pycache__/memory.cpython-312.pyc ADDED
Binary file (2.96 kB). View file
 
api/routes/__pycache__/tasks.cpython-312.pyc ADDED
Binary file (8.07 kB). View file
 
api/routes/chat.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Chat + Goal API Routes — Real-time streaming responses
3
+ """
4
+
5
+ import asyncio
6
+ import json
7
+ import time
8
+ import uuid
9
+
10
+ from fastapi import APIRouter, HTTPException, Request
11
+ from fastapi.responses import StreamingResponse
12
+
13
+ from core.models import ChatRequest, GoalRequest, TaskCreateRequest
14
+ from memory.db import save_memory, get_history
15
+
16
+ router = APIRouter()
17
+
18
+
19
+ def get_engine(request: Request):
20
+ return request.app.state.task_engine
21
+
22
+
23
+ def get_ws(request: Request):
24
+ return request.app.state.ws_manager
25
+
26
+
27
+ # ─── Chat (REST + SSE streaming) ───────────────────────────────────────────────
28
+
29
+ @router.post("/chat", summary="Chat with the agent")
30
+ async def chat(req: ChatRequest, request: Request):
31
+ from core.agent import AgentCore
32
+ ws = get_ws(request)
33
+ agent = AgentCore(ws)
34
+
35
+ messages = [{"role": m.role, "content": m.content} for m in req.messages]
36
+
37
+ if req.stream:
38
+ async def stream_gen():
39
+ async def _run():
40
+ result = await agent.llm_stream(
41
+ messages=messages,
42
+ session_id=req.session_id,
43
+ model=req.model,
44
+ temperature=req.temperature,
45
+ max_tokens=req.max_tokens,
46
+ )
47
+ await save_memory(
48
+ content=result,
49
+ memory_type="conversation",
50
+ session_id=req.session_id,
51
+ project_id=req.project_id,
52
+ key="assistant",
53
+ )
54
+ # Save user message too
55
+ user_msg = next((m["content"] for m in reversed(messages) if m["role"] == "user"), "")
56
+ await save_memory(
57
+ content=user_msg,
58
+ memory_type="conversation",
59
+ session_id=req.session_id,
60
+ project_id=req.project_id,
61
+ key="user",
62
+ )
63
+ return result
64
+
65
+ room_buffer = []
66
+ original_emit_chat = ws.emit_chat
67
+ async def capture_emit(sid, etype, data):
68
+ if etype == "llm_chunk":
69
+ chunk = data.get("chunk", "")
70
+ room_buffer.append(chunk)
71
+ yield_data = json.dumps({"type": etype, "data": data, "session_id": sid})
72
+ return yield_data
73
+ return None
74
+
75
+ # Stream tokens directly
76
+ full = ""
77
+ from core.agent import AgentCore as _A
78
+ import httpx
79
+ import os
80
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
81
+ ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
82
+
83
+ if OPENAI_API_KEY:
84
+ headers = {
85
+ "Authorization": f"Bearer {OPENAI_API_KEY}",
86
+ "Content-Type": "application/json",
87
+ }
88
+ payload = {
89
+ "model": req.model,
90
+ "messages": messages,
91
+ "stream": True,
92
+ "temperature": req.temperature,
93
+ "max_tokens": req.max_tokens,
94
+ }
95
+ from core.agent import OPENAI_BASE_URL
96
+ async with httpx.AsyncClient(timeout=120) as client:
97
+ async with client.stream("POST", f"{OPENAI_BASE_URL}/chat/completions",
98
+ headers=headers, json=payload) as resp:
99
+ async for line in resp.aiter_lines():
100
+ if not line.startswith("data:"):
101
+ continue
102
+ chunk_str = line[6:].strip()
103
+ if chunk_str == "[DONE]":
104
+ break
105
+ try:
106
+ data = json.loads(chunk_str)
107
+ delta = data["choices"][0]["delta"].get("content", "")
108
+ if delta:
109
+ full += delta
110
+ yield f"data: {json.dumps({'type': 'llm_chunk', 'data': {'chunk': delta}, 'session_id': req.session_id})}\n\n"
111
+ except Exception:
112
+ pass
113
+ else:
114
+ # Demo streaming
115
+ demo = (
116
+ f"Hello! I'm your Devin-style AI Agent. I received: '{req.messages[-1].content[:80]}'. "
117
+ f"Set OPENAI_API_KEY or ANTHROPIC_API_KEY for real AI responses. "
118
+ f"I support real-time streaming, task planning, GitHub automation, and more!"
119
+ )
120
+ for word in demo.split():
121
+ chunk = word + " "
122
+ full += chunk
123
+ await asyncio.sleep(0.04)
124
+ yield f"data: {json.dumps({'type': 'llm_chunk', 'data': {'chunk': chunk}, 'session_id': req.session_id})}\n\n"
125
+
126
+ yield f"data: {json.dumps({'type': 'stream_end', 'data': {'full_response': full}, 'session_id': req.session_id})}\n\n"
127
+
128
+ return StreamingResponse(
129
+ stream_gen(),
130
+ media_type="text/event-stream",
131
+ headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
132
+ )
133
+ else:
134
+ # Non-streaming
135
+ agent = AgentCore(get_ws(request))
136
+ result = await agent.llm_stream(messages, session_id=req.session_id)
137
+ return {
138
+ "response": result,
139
+ "session_id": req.session_id,
140
+ "model": req.model,
141
+ "timestamp": time.time(),
142
+ }
143
+
144
+
145
+ @router.post("/chat/stream", summary="Explicit streaming chat endpoint")
146
+ async def chat_stream(req: ChatRequest, request: Request):
147
+ req.stream = True
148
+ return await chat(req, request)
149
+
150
+
151
+ # ─── Goal API (create task from goal) ─────────────────────────────────────────
152
+
153
+ @router.post("/goal", summary="Submit a high-level goal to the agent")
154
+ async def submit_goal(req: GoalRequest, request: Request):
155
+ engine = get_engine(request)
156
+ task_req = TaskCreateRequest(
157
+ goal=req.goal,
158
+ session_id=req.session_id,
159
+ project_id=req.project_id,
160
+ stream=req.stream,
161
+ metadata={"source": "goal_api", "github_repo": req.github_repo},
162
+ )
163
+ task_id = await engine.submit(task_req)
164
+ return {
165
+ "task_id": task_id,
166
+ "goal": req.goal,
167
+ "status": "queued",
168
+ "session_id": req.session_id,
169
+ "ws_url": f"/ws/tasks/{task_id}",
170
+ "stream_url": f"/api/v1/tasks/{task_id}/stream",
171
+ }
172
+
173
+
174
+ @router.post("/goal/stream", summary="Submit goal with SSE streaming response")
175
+ async def submit_goal_stream(req: GoalRequest, request: Request):
176
+ req.stream = True
177
+ return await submit_goal(req, request)
178
+
179
+
180
+ # ─── Execute (direct tool execution) ──────────────────────────────────────────
181
+
182
+ @router.post("/execute", summary="Execute a tool directly")
183
+ async def execute(
184
+ tool: str,
185
+ task: str,
186
+ request: Request,
187
+ session_id: str = "",
188
+ ):
189
+ from tools.executor import ToolExecutor
190
+ ws = get_ws(request)
191
+ executor = ToolExecutor(ws)
192
+ result = await executor.run(
193
+ tool=tool,
194
+ task=task,
195
+ session_id=session_id,
196
+ )
197
+ return {"tool": tool, "task": task, "result": result, "session_id": session_id}
198
+
199
+
200
+ # ─── Plan (generate plan without executing) ───────────────────────────────────
201
+
202
+ @router.post("/plan", summary="Generate execution plan for a goal")
203
+ async def generate_plan(req: GoalRequest, request: Request):
204
+ from core.agent import AgentCore
205
+ ws = get_ws(request)
206
+ agent = AgentCore(ws)
207
+ task_id = f"plan_{uuid.uuid4().hex[:8]}"
208
+ plan = await agent.plan(goal=req.goal, task_id=task_id, session_id=req.session_id)
209
+ return {
210
+ "goal": req.goal,
211
+ "plan": plan.model_dump(),
212
+ "session_id": req.session_id,
213
+ "task_id": task_id,
214
+ }
api/routes/github.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GitHub Autonomous Engineering API Routes
3
+ Clone, commit, push, PR, issues — all autonomous
4
+ """
5
+
6
+ import os
7
+ import time
8
+ import asyncio
9
+ import tempfile
10
+ import shutil
11
+ from typing import Optional
12
+
13
+ import httpx
14
+ from fastapi import APIRouter, HTTPException, Request
15
+
16
+ from core.models import (
17
+ GitHubCloneRequest, GitHubCreateRepoRequest,
18
+ GitHubCommitRequest, GitHubPRRequest, GitHubIssueRequest,
19
+ )
20
+ from memory.db import save_memory
21
+
22
+ router = APIRouter()
23
+
24
+ GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "")
25
+ GITHUB_OWNER = os.environ.get("GITHUB_OWNER", "")
26
+ GITHUB_API = "https://api.github.com"
27
+
28
+
29
+ def gh_headers():
30
+ if not GITHUB_TOKEN:
31
+ raise HTTPException(status_code=400, detail="GITHUB_TOKEN not configured")
32
+ return {
33
+ "Authorization": f"Bearer {GITHUB_TOKEN}",
34
+ "Accept": "application/vnd.github+json",
35
+ "X-GitHub-Api-Version": "2022-11-28",
36
+ }
37
+
38
+
39
+ async def gh_get(path: str) -> dict:
40
+ async with httpx.AsyncClient(timeout=30) as client:
41
+ r = await client.get(f"{GITHUB_API}{path}", headers=gh_headers())
42
+ r.raise_for_status()
43
+ return r.json()
44
+
45
+
46
+ async def gh_post(path: str, data: dict) -> dict:
47
+ async with httpx.AsyncClient(timeout=30) as client:
48
+ r = await client.post(f"{GITHUB_API}{path}", headers=gh_headers(), json=data)
49
+ r.raise_for_status()
50
+ return r.json()
51
+
52
+
53
+ async def gh_put(path: str, data: dict) -> dict:
54
+ async with httpx.AsyncClient(timeout=30) as client:
55
+ r = await client.put(f"{GITHUB_API}{path}", headers=gh_headers(), json=data)
56
+ r.raise_for_status()
57
+ return r.json()
58
+
59
+
60
+ async def gh_patch(path: str, data: dict) -> dict:
61
+ async with httpx.AsyncClient(timeout=30) as client:
62
+ r = await client.patch(f"{GITHUB_API}{path}", headers=gh_headers(), json=data)
63
+ r.raise_for_status()
64
+ return r.json()
65
+
66
+
67
+ # ─── Clone ────────────────────────────────────────────────────────────────────
68
+
69
+ @router.post("/clone", summary="Clone a GitHub repository")
70
+ async def clone_repo(req: GitHubCloneRequest):
71
+ try:
72
+ import git
73
+ except ImportError:
74
+ raise HTTPException(status_code=500, detail="gitpython not installed")
75
+
76
+ local_path = req.local_path or f"/tmp/repos/{req.repo_url.split('/')[-1].replace('.git', '')}"
77
+ os.makedirs(local_path, exist_ok=True)
78
+
79
+ if GITHUB_TOKEN:
80
+ url = req.repo_url.replace("https://", f"https://{GITHUB_TOKEN}@")
81
+ else:
82
+ url = req.repo_url
83
+
84
+ try:
85
+ if os.path.exists(os.path.join(local_path, ".git")):
86
+ repo = git.Repo(local_path)
87
+ repo.remotes.origin.pull()
88
+ action = "pulled"
89
+ else:
90
+ repo = git.Repo.clone_from(url, local_path, branch=req.branch, depth=1)
91
+ action = "cloned"
92
+
93
+ files = []
94
+ for root, dirs, fnames in os.walk(local_path):
95
+ dirs[:] = [d for d in dirs if d not in [".git", "node_modules", "__pycache__"]]
96
+ for f in fnames[:50]:
97
+ files.append(os.path.relpath(os.path.join(root, f), local_path))
98
+
99
+ # Save to memory
100
+ await save_memory(
101
+ content=f"Repo {req.repo_url} cloned to {local_path}. Files: {', '.join(files[:20])}",
102
+ memory_type="repo",
103
+ key=req.repo_url,
104
+ )
105
+
106
+ return {
107
+ "action": action,
108
+ "repo_url": req.repo_url,
109
+ "local_path": local_path,
110
+ "branch": req.branch,
111
+ "files_count": len(files),
112
+ "files": files[:30],
113
+ }
114
+ except Exception as e:
115
+ raise HTTPException(status_code=500, detail=f"Clone failed: {str(e)}")
116
+
117
+
118
+ # ─── Create Repo ──────────────────────────────────────────────────────────────
119
+
120
+ @router.post("/create_repo", summary="Create a new GitHub repository")
121
+ async def create_repo(req: GitHubCreateRepoRequest):
122
+ data = {
123
+ "name": req.name,
124
+ "description": req.description,
125
+ "private": req.private,
126
+ "auto_init": req.auto_init,
127
+ }
128
+ try:
129
+ result = await gh_post("/user/repos", data)
130
+ return {
131
+ "repo": result["full_name"],
132
+ "url": result["html_url"],
133
+ "clone_url": result["clone_url"],
134
+ "default_branch": result.get("default_branch", "main"),
135
+ "private": result["private"],
136
+ }
137
+ except httpx.HTTPStatusError as e:
138
+ raise HTTPException(status_code=e.response.status_code, detail=e.response.text)
139
+
140
+
141
+ # ─── Commit Files ─────────────────────────────────────────────────────────────
142
+
143
+ @router.post("/commit", summary="Commit files to a repository")
144
+ async def commit_files(req: GitHubCommitRequest):
145
+ import base64
146
+
147
+ owner_repo = req.repo if "/" in req.repo else f"{GITHUB_OWNER}/{req.repo}"
148
+ results = []
149
+
150
+ for file_path, content in req.files.items():
151
+ encoded = base64.b64encode(content.encode()).decode()
152
+
153
+ # Get current SHA if file exists
154
+ sha = None
155
+ try:
156
+ existing = await gh_get(f"/repos/{owner_repo}/contents/{file_path}?ref={req.branch}")
157
+ sha = existing.get("sha")
158
+ except Exception:
159
+ pass
160
+
161
+ payload = {
162
+ "message": req.message,
163
+ "content": encoded,
164
+ "branch": req.branch,
165
+ }
166
+ if sha:
167
+ payload["sha"] = sha
168
+
169
+ try:
170
+ result = await gh_put(f"/repos/{owner_repo}/contents/{file_path}", payload)
171
+ results.append({"file": file_path, "status": "committed", "sha": result["content"]["sha"]})
172
+ except Exception as e:
173
+ results.append({"file": file_path, "status": "error", "error": str(e)})
174
+
175
+ return {
176
+ "repo": owner_repo,
177
+ "branch": req.branch,
178
+ "message": req.message,
179
+ "files": results,
180
+ "committed": sum(1 for r in results if r["status"] == "committed"),
181
+ }
182
+
183
+
184
+ # ─── Push ─────────────────────────────────────────────────────────────────────
185
+
186
+ @router.post("/push", summary="Push local changes to remote")
187
+ async def push_changes(
188
+ repo_path: str,
189
+ branch: str = "main",
190
+ message: str = "Auto-commit by Devin Agent",
191
+ ):
192
+ try:
193
+ import git
194
+ repo = git.Repo(repo_path)
195
+ repo.git.add(A=True)
196
+ if repo.index.diff("HEAD") or repo.untracked_files:
197
+ repo.index.commit(message)
198
+ origin = repo.remote("origin")
199
+ origin.push(refspec=f"HEAD:{branch}")
200
+ return {"status": "pushed", "branch": branch, "message": message}
201
+ except Exception as e:
202
+ raise HTTPException(status_code=500, detail=f"Push failed: {str(e)}")
203
+
204
+
205
+ # ─── Create PR ────────────────────────────────────────────────────────────────
206
+
207
+ @router.post("/pr/create", summary="Create a Pull Request")
208
+ async def create_pr(req: GitHubPRRequest):
209
+ owner_repo = req.repo if "/" in req.repo else f"{GITHUB_OWNER}/{req.repo}"
210
+ data = {
211
+ "title": req.title,
212
+ "body": req.body,
213
+ "head": req.head,
214
+ "base": req.base,
215
+ "draft": req.draft,
216
+ }
217
+ try:
218
+ result = await gh_post(f"/repos/{owner_repo}/pulls", data)
219
+ return {
220
+ "pr_number": result["number"],
221
+ "title": result["title"],
222
+ "url": result["html_url"],
223
+ "state": result["state"],
224
+ "head": req.head,
225
+ "base": req.base,
226
+ }
227
+ except httpx.HTTPStatusError as e:
228
+ raise HTTPException(status_code=e.response.status_code, detail=e.response.text)
229
+
230
+
231
+ # ─── Create Issue ─────────────────────────────────────────────────────────────
232
+
233
+ @router.post("/issues/create", summary="Create a GitHub Issue")
234
+ async def create_issue(req: GitHubIssueRequest):
235
+ owner_repo = req.repo if "/" in req.repo else f"{GITHUB_OWNER}/{req.repo}"
236
+ data = {"title": req.title, "body": req.body, "labels": req.labels}
237
+ try:
238
+ result = await gh_post(f"/repos/{owner_repo}/issues", data)
239
+ return {
240
+ "issue_number": result["number"],
241
+ "title": result["title"],
242
+ "url": result["html_url"],
243
+ "state": result["state"],
244
+ }
245
+ except httpx.HTTPStatusError as e:
246
+ raise HTTPException(status_code=e.response.status_code, detail=e.response.text)
247
+
248
+
249
+ # ─── Code Review ──────────────────────────────────────────────────────────────
250
+
251
+ @router.post("/review", summary="AI code review for a PR")
252
+ async def review_pr(repo: str, pr_number: int, request: Request):
253
+ owner_repo = repo if "/" in repo else f"{GITHUB_OWNER}/{repo}"
254
+ try:
255
+ pr = await gh_get(f"/repos/{owner_repo}/pulls/{pr_number}")
256
+ files = await gh_get(f"/repos/{owner_repo}/pulls/{pr_number}/files")
257
+
258
+ file_changes = []
259
+ for f in files[:10]:
260
+ file_changes.append(f"{f['filename']}: +{f.get('additions',0)}/-{f.get('deletions',0)}")
261
+
262
+ ws = request.app.state.ws_manager
263
+ from core.agent import AgentCore
264
+ agent = AgentCore(ws)
265
+
266
+ review_prompt = (
267
+ f"Review this Pull Request:\n"
268
+ f"Title: {pr['title']}\n"
269
+ f"Description: {pr.get('body', 'No description')}\n"
270
+ f"Files changed: {chr(10).join(file_changes)}\n\n"
271
+ f"Provide a constructive code review with: summary, potential issues, suggestions, and verdict."
272
+ )
273
+ messages = [
274
+ {"role": "system", "content": "You are a senior software engineer doing code review. Be constructive, specific, and helpful."},
275
+ {"role": "user", "content": review_prompt},
276
+ ]
277
+ review = await agent.llm_stream(messages)
278
+
279
+ # Post review comment
280
+ if GITHUB_TOKEN:
281
+ await gh_post(f"/repos/{owner_repo}/issues/{pr_number}/comments", {"body": f"🤖 **Devin Agent Code Review**\n\n{review}"})
282
+
283
+ return {
284
+ "pr_number": pr_number,
285
+ "title": pr["title"],
286
+ "review": review,
287
+ "files_reviewed": len(files),
288
+ "posted_to_github": bool(GITHUB_TOKEN),
289
+ }
290
+ except Exception as e:
291
+ raise HTTPException(status_code=500, detail=str(e))
292
+
293
+
294
+ # ─── Repo Info ────────────────────────────────────────────────────────────────
295
+
296
+ @router.get("/repo/{owner}/{repo}", summary="Get repository info")
297
+ async def get_repo_info(owner: str, repo: str):
298
+ try:
299
+ info = await gh_get(f"/repos/{owner}/{repo}")
300
+ return {
301
+ "name": info["name"],
302
+ "full_name": info["full_name"],
303
+ "description": info.get("description"),
304
+ "url": info["html_url"],
305
+ "default_branch": info["default_branch"],
306
+ "language": info.get("language"),
307
+ "stars": info["stargazers_count"],
308
+ "forks": info["forks_count"],
309
+ "open_issues": info["open_issues_count"],
310
+ "private": info["private"],
311
+ }
312
+ except httpx.HTTPStatusError as e:
313
+ raise HTTPException(status_code=e.response.status_code, detail=e.response.text)
314
+
315
+
316
+ # ─── Status check ─────────────────────────────────────────────────────────────
317
+
318
+ @router.get("/status", summary="GitHub integration status")
319
+ async def github_status():
320
+ configured = bool(GITHUB_TOKEN)
321
+ user = None
322
+ if configured:
323
+ try:
324
+ user_info = await gh_get("/user")
325
+ user = user_info.get("login")
326
+ except Exception:
327
+ configured = False
328
+ return {
329
+ "configured": configured,
330
+ "user": user,
331
+ "owner": GITHUB_OWNER or user,
332
+ "capabilities": [
333
+ "clone", "create_repo", "commit", "push",
334
+ "pr/create", "issues/create", "review"
335
+ ],
336
+ }
api/routes/health.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Health + Status Routes
3
+ """
4
+
5
+ import time
6
+ import os
7
+ import psutil
8
+ from fastapi import APIRouter, Request
9
+
10
+ router = APIRouter()
11
+
12
+
13
+ @router.get("/health", summary="Health check")
14
+ async def health(request: Request):
15
+ ws = request.app.state.ws_manager
16
+ engine = request.app.state.task_engine
17
+ stats = ws.get_stats()
18
+ return {
19
+ "status": "healthy",
20
+ "version": "2.0.0",
21
+ "timestamp": time.time(),
22
+ "websocket_connections": stats["total_connections"],
23
+ "websocket_rooms": list(stats["rooms"].keys()),
24
+ "task_queue_size": engine._queue.qsize(),
25
+ "active_tasks": len(engine._active),
26
+ "llm": {
27
+ "openai": bool(os.environ.get("OPENAI_API_KEY")),
28
+ "anthropic": bool(os.environ.get("ANTHROPIC_API_KEY")),
29
+ "model": os.environ.get("DEFAULT_MODEL", "gpt-4o"),
30
+ },
31
+ "github": bool(os.environ.get("GITHUB_TOKEN")),
32
+ }
33
+
34
+
35
+ @router.get("/metrics", summary="System metrics")
36
+ async def metrics():
37
+ cpu = psutil.cpu_percent(interval=0.1)
38
+ mem = psutil.virtual_memory()
39
+ disk = psutil.disk_usage("/")
40
+ return {
41
+ "cpu_percent": cpu,
42
+ "memory": {
43
+ "total_mb": round(mem.total / 1024 / 1024),
44
+ "used_mb": round(mem.used / 1024 / 1024),
45
+ "percent": mem.percent,
46
+ },
47
+ "disk": {
48
+ "total_gb": round(disk.total / 1024 / 1024 / 1024, 1),
49
+ "used_gb": round(disk.used / 1024 / 1024 / 1024, 1),
50
+ "percent": disk.percent,
51
+ },
52
+ "timestamp": time.time(),
53
+ }
api/routes/memory.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Memory API Routes — Persistent agent memory
3
+ """
4
+
5
+ import time
6
+ from fastapi import APIRouter, HTTPException, Query
7
+ from core.models import MemorySaveRequest, MemorySearchRequest
8
+ from memory.db import save_memory, search_memory, get_project_memory, get_history
9
+
10
+ router = APIRouter()
11
+
12
+
13
+ @router.post("/", summary="Save memory")
14
+ async def save(req: MemorySaveRequest):
15
+ await save_memory(
16
+ content=req.content,
17
+ memory_type=req.memory_type.value,
18
+ session_id=req.session_id,
19
+ project_id=req.project_id,
20
+ key=req.key,
21
+ metadata=req.metadata,
22
+ )
23
+ return {"status": "saved", "memory_type": req.memory_type, "timestamp": time.time()}
24
+
25
+
26
+ @router.post("/search", summary="Search memory")
27
+ async def search(req: MemorySearchRequest):
28
+ results = await search_memory(
29
+ query=req.query,
30
+ session_id=req.session_id,
31
+ project_id=req.project_id,
32
+ limit=req.limit,
33
+ )
34
+ return {"results": results, "total": len(results), "query": req.query}
35
+
36
+
37
+ @router.get("/project/{project_id}", summary="Get project memory")
38
+ async def project_memory(
39
+ project_id: str,
40
+ memory_type: str = Query(default=""),
41
+ limit: int = Query(default=100, le=500),
42
+ ):
43
+ results = await get_project_memory(project_id, memory_type=memory_type, limit=limit)
44
+ return {"project_id": project_id, "memories": results, "total": len(results)}
45
+
46
+
47
+ @router.get("/history/{session_id}", summary="Get conversation history")
48
+ async def history(session_id: str, limit: int = Query(default=50, le=200)):
49
+ results = await get_history(session_id, limit=limit)
50
+ return {"session_id": session_id, "history": results, "total": len(results)}
api/routes/tasks.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Task API Routes — CRUD + Streaming + WebSocket
3
+ """
4
+
5
+ import asyncio
6
+ import json
7
+ import time
8
+ from typing import Optional
9
+
10
+ from fastapi import APIRouter, HTTPException, Request, Query
11
+ from fastapi.responses import StreamingResponse
12
+
13
+ from core.models import (
14
+ TaskCreateRequest, TaskCancelRequest, TaskRetryRequest, TaskResponse, TaskStatus
15
+ )
16
+ from memory.db import get_task, list_tasks, get_task_events, update_task_status
17
+
18
+ router = APIRouter()
19
+
20
+
21
+ def get_engine(request: Request):
22
+ return request.app.state.task_engine
23
+
24
+
25
+ def get_ws(request: Request):
26
+ return request.app.state.ws_manager
27
+
28
+
29
+ # ─── Create Task ───────────────────────────────────────────────────────────────
30
+
31
+ @router.post("/create", summary="Create & queue a new agent task")
32
+ async def create_task(req: TaskCreateRequest, request: Request):
33
+ engine = get_engine(request)
34
+ task_id = await engine.submit(req)
35
+ task = await get_task(task_id)
36
+ return {
37
+ "task_id": task_id,
38
+ "status": "queued",
39
+ "goal": req.goal,
40
+ "session_id": req.session_id,
41
+ "stream_url": f"/api/v1/tasks/{task_id}/stream",
42
+ "ws_url": f"/ws/tasks/{task_id}",
43
+ "created_at": task["created_at"] if task else time.time(),
44
+ }
45
+
46
+
47
+ # ─── Get Task ──────────────────────────────────────────────────────────────────
48
+
49
+ @router.get("/{task_id}", summary="Get task details")
50
+ async def get_task_detail(task_id: str):
51
+ task = await get_task(task_id)
52
+ if not task:
53
+ raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
54
+ return task
55
+
56
+
57
+ # ─── Get Task Status ───────────────────────────────────────────────────────────
58
+
59
+ @router.get("/{task_id}/status", summary="Get task status only")
60
+ async def get_task_status(task_id: str):
61
+ task = await get_task(task_id)
62
+ if not task:
63
+ raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
64
+ return {
65
+ "task_id": task_id,
66
+ "status": task["status"],
67
+ "retry_count": task.get("retry_count", 0),
68
+ "created_at": task.get("created_at"),
69
+ "started_at": task.get("started_at"),
70
+ "completed_at": task.get("completed_at"),
71
+ }
72
+
73
+
74
+ # ─── Cancel Task ───────────────────────────────────────────────────────────────
75
+
76
+ @router.post("/{task_id}/cancel", summary="Cancel a running task")
77
+ async def cancel_task(task_id: str, req: TaskCancelRequest, request: Request):
78
+ task = await get_task(task_id)
79
+ if not task:
80
+ raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
81
+ if task["status"] in ("completed", "failed", "cancelled"):
82
+ raise HTTPException(status_code=400, detail=f"Task already {task['status']}")
83
+ engine = get_engine(request)
84
+ await engine.cancel(task_id, req.reason)
85
+ return {"task_id": task_id, "status": "cancelled", "reason": req.reason}
86
+
87
+
88
+ # ─── Retry Task ────────────────────────────────────────────────────────────────
89
+
90
+ @router.post("/{task_id}/retry", summary="Retry a failed task")
91
+ async def retry_task(task_id: str, request: Request):
92
+ task = await get_task(task_id)
93
+ if not task:
94
+ raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
95
+ if task["status"] not in ("failed", "cancelled"):
96
+ raise HTTPException(status_code=400, detail="Only failed/cancelled tasks can be retried")
97
+ engine = get_engine(request)
98
+ await engine.retry(task_id)
99
+ return {"task_id": task_id, "status": "queued", "message": "Task requeued for retry"}
100
+
101
+
102
+ # ─── Stream Task Events (SSE) ──────────────────────────────────────────────────
103
+
104
+ @router.get("/{task_id}/stream", summary="Stream task events via SSE")
105
+ async def stream_task(task_id: str, request: Request):
106
+ task = await get_task(task_id)
107
+ if not task:
108
+ raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
109
+
110
+ async def event_generator():
111
+ # First, replay all stored events
112
+ events = await get_task_events(task_id)
113
+ for ev in events:
114
+ data = json.dumps({
115
+ "type": ev["event_type"],
116
+ "task_id": task_id,
117
+ "timestamp": ev["timestamp"],
118
+ "data": json.loads(ev["data"]) if ev.get("data") else {},
119
+ })
120
+ yield f"data: {data}\n\n"
121
+
122
+ # Then stream live events via WS manager buffer
123
+ ws = get_ws(request)
124
+ room = f"task:{task_id}"
125
+ last_count = len(events)
126
+
127
+ # Poll for new events (for SSE fallback)
128
+ for _ in range(600): # max 5 minutes
129
+ await asyncio.sleep(0.5)
130
+ current_task = await get_task(task_id)
131
+ if current_task and current_task["status"] in ("completed", "failed", "cancelled"):
132
+ yield f"data: {json.dumps({'type': 'stream_end', 'task_id': task_id, 'status': current_task['status']})}\n\n"
133
+ break
134
+ # heartbeat
135
+ yield f"data: {json.dumps({'type': 'heartbeat', 'timestamp': time.time()})}\n\n"
136
+
137
+ return StreamingResponse(
138
+ event_generator(),
139
+ media_type="text/event-stream",
140
+ headers={
141
+ "Cache-Control": "no-cache",
142
+ "X-Accel-Buffering": "no",
143
+ "Connection": "keep-alive",
144
+ },
145
+ )
146
+
147
+
148
+ # ─── List Tasks ────────────────────────────────────────────────────────────────
149
+
150
+ @router.get("/", summary="List tasks")
151
+ async def list_all_tasks(
152
+ session_id: str = Query(default=""),
153
+ limit: int = Query(default=50, le=200),
154
+ ):
155
+ tasks = await list_tasks(session_id=session_id, limit=limit)
156
+ return {"tasks": tasks, "total": len(tasks)}
157
+
158
+
159
+ # ─── Task Events History ───────────────────────────────────────────────────────
160
+
161
+ @router.get("/{task_id}/events", summary="Get all events for a task")
162
+ async def task_events(task_id: str):
163
+ task = await get_task(task_id)
164
+ if not task:
165
+ raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
166
+ events = await get_task_events(task_id)
167
+ return {"task_id": task_id, "events": events, "total": len(events)}
api/websocket_manager.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ WebSocket Connection Manager — Production Grade
3
+ Handles rooms, heartbeats, event buffering, reconnect support
4
+ """
5
+
6
+ import asyncio
7
+ import json
8
+ import time
9
+ import uuid
10
+ from collections import defaultdict
11
+ from typing import Dict, List, Optional, Set
12
+ import structlog
13
+
14
+ log = structlog.get_logger()
15
+
16
+
17
+ class WebSocketManager:
18
+ def __init__(self):
19
+ # room → set of websockets
20
+ self._rooms: Dict[str, Set] = defaultdict(set)
21
+ # ws → list of rooms
22
+ self._ws_rooms: Dict[object, Set[str]] = defaultdict(set)
23
+ # Event buffer per room (for replay on reconnect)
24
+ self._event_buffer: Dict[str, List] = defaultdict(list)
25
+ self._buffer_max = 100
26
+ # Active connection count
27
+ self._connection_count = 0
28
+
29
+ async def connect(self, websocket, room: str):
30
+ await websocket.accept()
31
+ self._rooms[room].add(websocket)
32
+ self._ws_rooms[websocket].add(room)
33
+ self._connection_count += 1
34
+ log.info("WS connected", room=room, total=self._connection_count)
35
+
36
+ # Replay buffered events for this room
37
+ buffered = self._event_buffer.get(room, [])[-20:]
38
+ for event in buffered:
39
+ try:
40
+ await websocket.send_json(event)
41
+ except Exception:
42
+ pass
43
+
44
+ await websocket.send_json({
45
+ "type": "connected",
46
+ "room": room,
47
+ "timestamp": time.time(),
48
+ "buffered_events": len(buffered),
49
+ })
50
+
51
+ def disconnect(self, websocket, room: Optional[str] = None):
52
+ if room:
53
+ self._rooms[room].discard(websocket)
54
+ self._ws_rooms[websocket].discard(room)
55
+ else:
56
+ for r in list(self._ws_rooms.get(websocket, [])):
57
+ self._rooms[r].discard(websocket)
58
+ self._ws_rooms.pop(websocket, None)
59
+ self._connection_count = max(0, self._connection_count - 1)
60
+ log.info("WS disconnected", room=room, total=self._connection_count)
61
+
62
+ async def broadcast(self, room: str, event: dict):
63
+ """Broadcast event to all sockets in a room."""
64
+ if "timestamp" not in event:
65
+ event["timestamp"] = time.time()
66
+ if "id" not in event:
67
+ event["id"] = str(uuid.uuid4())[:8]
68
+
69
+ # Buffer event
70
+ self._event_buffer[room].append(event)
71
+ if len(self._event_buffer[room]) > self._buffer_max:
72
+ self._event_buffer[room].pop(0)
73
+
74
+ dead = set()
75
+ for ws in list(self._rooms.get(room, [])):
76
+ try:
77
+ await ws.send_json(event)
78
+ except Exception:
79
+ dead.add(ws)
80
+
81
+ for ws in dead:
82
+ self.disconnect(ws, room)
83
+
84
+ async def broadcast_global(self, event: dict):
85
+ """Broadcast to ALL connected websockets."""
86
+ for room in list(self._rooms.keys()):
87
+ await self.broadcast(room, event)
88
+
89
+ async def emit(self, task_id: str, event_type: str, data: dict, session_id: str = ""):
90
+ """Emit a structured event to a task room + logs room."""
91
+ event = {
92
+ "type": event_type,
93
+ "task_id": task_id,
94
+ "session_id": session_id,
95
+ "timestamp": time.time(),
96
+ "data": data,
97
+ }
98
+ await self.broadcast(f"task:{task_id}", event)
99
+ await self.broadcast("logs", event)
100
+ await self.broadcast("agent_status", {
101
+ "type": "agent_event",
102
+ "task_id": task_id,
103
+ "event_type": event_type,
104
+ "timestamp": time.time(),
105
+ })
106
+
107
+ async def emit_chat(self, session_id: str, event_type: str, data: dict):
108
+ """Emit event to a chat session room."""
109
+ event = {
110
+ "type": event_type,
111
+ "session_id": session_id,
112
+ "timestamp": time.time(),
113
+ "data": data,
114
+ }
115
+ await self.broadcast(f"chat:{session_id}", event)
116
+
117
+ async def heartbeat_loop(self):
118
+ """Send heartbeat to all connections every 15s."""
119
+ while True:
120
+ await asyncio.sleep(15)
121
+ heartbeat = {
122
+ "type": "heartbeat",
123
+ "timestamp": time.time(),
124
+ "connections": self._connection_count,
125
+ }
126
+ for room in list(self._rooms.keys()):
127
+ await self.broadcast(room, heartbeat)
128
+
129
+ def get_stats(self) -> dict:
130
+ return {
131
+ "total_connections": self._connection_count,
132
+ "rooms": {r: len(ws) for r, ws in self._rooms.items()},
133
+ "buffered_events": {r: len(e) for r, e in self._event_buffer.items()},
134
+ }
api_server.py DELETED
@@ -1,44 +0,0 @@
1
- import os
2
- import uvicorn
3
- from fastapi import FastAPI, Depends, HTTPException, status
4
- from fastapi.middleware.cors import CORSMiddleware
5
- from routes import chat, tasks, github, workspace, browser, swarm, webhooks
6
- from auth import get_api_key
7
- import websocket_server
8
-
9
- app = FastAPI(title="Autonomous Coding System API", version="1.0.0")
10
-
11
- # CORS Configuration
12
- app.add_middleware(
13
- CORSMiddleware,
14
- allow_origins=["*"],
15
- allow_credentials=True,
16
- allow_methods=["*"],
17
- allow_headers=["*"],
18
- )
19
-
20
- # Include Routes
21
- app.include_router(chat.router, prefix="/api/v1", tags=["Chat"], dependencies=[Depends(get_api_key)])
22
- app.include_router(tasks.router, prefix="/api/v1", tags=["Tasks"], dependencies=[Depends(get_api_key)])
23
- app.include_router(github.router, prefix="/api/v1", tags=["GitHub"], dependencies=[Depends(get_api_key)])
24
- app.include_router(workspace.router, prefix="/api/v1", tags=["Workspace"], dependencies=[Depends(get_api_key)])
25
- app.include_router(browser.router, prefix="/api/v1", tags=["Browser"], dependencies=[Depends(get_api_key)])
26
- app.include_router(swarm.router, prefix="/api/v1", tags=["Swarm"], dependencies=[Depends(get_api_key)])
27
- app.include_router(webhooks.router, prefix="/api/v1", tags=["Webhooks"])
28
-
29
- @app.get("/health")
30
- async def health_check():
31
- return {"status": "healthy", "timestamp": "now"}
32
-
33
- @app.get("/agent/status")
34
- async def agent_status():
35
- return {
36
- "status": "idle",
37
- "active_tasks": 0,
38
- "memory_usage": "low",
39
- "uptime": "0s"
40
- }
41
-
42
- if __name__ == "__main__":
43
- port = int(os.getenv("API_PORT", 8000))
44
- uvicorn.run(app, host="0.0.0.0", port=port)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
auth.py DELETED
@@ -1,41 +0,0 @@
1
- import os
2
- from fastapi import Security, HTTPException, status
3
- from fastapi.security.api_key import APIKeyHeader
4
- from jose import JWTError, jwt
5
- from datetime import datetime, timedelta
6
- from typing import Optional
7
-
8
- API_KEY_NAME = "X-API-Key"
9
- api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
10
-
11
- SECRET_KEY = os.getenv("JWT_SECRET_KEY", "super-secret-key")
12
- ALGORITHM = "HS256"
13
- ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 1 week
14
-
15
- def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
16
- to_encode = data.copy()
17
- if expires_delta:
18
- expire = datetime.utcnow() + expires_delta
19
- else:
20
- expire = datetime.utcnow() + timedelta(minutes=15)
21
- to_encode.update({"exp": expire})
22
- encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
23
- return encoded_jwt
24
-
25
- async def get_api_key(api_key_header: str = Security(api_key_header)):
26
- if api_key_header == os.getenv("API_KEY", "default-api-key"):
27
- return api_key_header
28
- raise HTTPException(
29
- status_code=status.HTTP_403_FORBIDDEN, detail="Could not validate API Key"
30
- )
31
-
32
- async def verify_token(token: str):
33
- try:
34
- payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
35
- return payload
36
- except JWTError:
37
- raise HTTPException(
38
- status_code=status.HTTP_401_UNAUTHORIZED,
39
- detail="Could not validate credentials",
40
- headers={"WWW-Authenticate": "Bearer"},
41
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/__init__.py CHANGED
@@ -1 +0,0 @@
1
- # Core logic package
 
 
core/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (144 Bytes). View file
 
core/__pycache__/agent.cpython-312.pyc ADDED
Binary file (17.2 kB). View file
 
core/__pycache__/models.cpython-312.pyc ADDED
Binary file (9.79 kB). View file
 
core/__pycache__/task_engine.cpython-312.pyc ADDED
Binary file (14.5 kB). View file
 
core/agent.py ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agent Core — Planner + Executor + Self-Heal Loop
3
+ LLM-powered with OpenAI/Anthropic support, streaming tokens
4
+ """
5
+
6
+ import asyncio
7
+ import json
8
+ import os
9
+ import time
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ import httpx
13
+ import structlog
14
+
15
+ from core.models import TaskPlan, TaskStep
16
+ from api.websocket_manager import WebSocketManager
17
+ from memory.db import save_memory, get_history, search_memory
18
+
19
+ log = structlog.get_logger()
20
+
21
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
22
+ ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
23
+ DEFAULT_MODEL = os.environ.get("DEFAULT_MODEL", "gpt-4o")
24
+ OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1")
25
+
26
+
27
+ SYSTEM_PROMPT = """You are an elite autonomous AI software engineer — like Devin or Manus.
28
+ You can plan, code, debug, refactor, test, and deploy software autonomously.
29
+ You think step-by-step, write production-quality code, and self-heal on errors.
30
+ Always respond in structured JSON when asked for plans or structured output.
31
+ """
32
+
33
+ PLANNER_PROMPT = """You are a senior software architect. Given a goal, produce a detailed execution plan.
34
+
35
+ Respond ONLY with valid JSON:
36
+ {
37
+ "steps": [
38
+ {
39
+ "name": "Step name",
40
+ "description": "What this step does",
41
+ "tool": "code|shell|file|browser|github|memory|search|test|none",
42
+ "estimated_seconds": 10
43
+ }
44
+ ],
45
+ "estimated_duration": 60,
46
+ "tools_needed": ["code", "shell"]
47
+ }
48
+
49
+ Goal: {goal}
50
+ Context: {context}
51
+ """
52
+
53
+
54
+ class AgentCore:
55
+ def __init__(self, ws_manager: WebSocketManager):
56
+ self.ws = ws_manager
57
+ self.model = DEFAULT_MODEL
58
+
59
+ # ─── LLM Call (with streaming) ─────────────────────────────────────────────
60
+
61
+ async def llm_stream(
62
+ self,
63
+ messages: List[Dict],
64
+ task_id: str = "",
65
+ session_id: str = "",
66
+ model: str = "",
67
+ temperature: float = 0.7,
68
+ max_tokens: int = 4096,
69
+ ) -> str:
70
+ """Stream LLM tokens, emitting llm_chunk events via WebSocket."""
71
+ model = model or self.model
72
+ full_text = ""
73
+
74
+ if OPENAI_API_KEY:
75
+ full_text = await self._openai_stream(
76
+ messages, task_id, session_id, model, temperature, max_tokens
77
+ )
78
+ elif ANTHROPIC_API_KEY:
79
+ full_text = await self._anthropic_stream(
80
+ messages, task_id, session_id, temperature, max_tokens
81
+ )
82
+ else:
83
+ # Demo mode — simulate streaming
84
+ full_text = await self._demo_stream(messages, task_id, session_id)
85
+
86
+ return full_text
87
+
88
+ async def _openai_stream(
89
+ self, messages, task_id, session_id, model, temperature, max_tokens
90
+ ) -> str:
91
+ full_text = ""
92
+ headers = {
93
+ "Authorization": f"Bearer {OPENAI_API_KEY}",
94
+ "Content-Type": "application/json",
95
+ }
96
+ payload = {
97
+ "model": model,
98
+ "messages": messages,
99
+ "stream": True,
100
+ "temperature": temperature,
101
+ "max_tokens": max_tokens,
102
+ }
103
+ async with httpx.AsyncClient(timeout=120) as client:
104
+ async with client.stream(
105
+ "POST", f"{OPENAI_BASE_URL}/chat/completions",
106
+ headers=headers, json=payload
107
+ ) as resp:
108
+ resp.raise_for_status()
109
+ async for line in resp.aiter_lines():
110
+ if not line.startswith("data:"):
111
+ continue
112
+ chunk = line[6:].strip()
113
+ if chunk == "[DONE]":
114
+ break
115
+ try:
116
+ data = json.loads(chunk)
117
+ delta = data["choices"][0]["delta"].get("content", "")
118
+ if delta:
119
+ full_text += delta
120
+ if task_id:
121
+ await self.ws.emit(task_id, "llm_chunk", {
122
+ "chunk": delta,
123
+ "accumulated": len(full_text),
124
+ }, session_id=session_id)
125
+ if session_id and not task_id:
126
+ await self.ws.emit_chat(session_id, "llm_chunk", {
127
+ "chunk": delta,
128
+ })
129
+ except Exception:
130
+ pass
131
+ return full_text
132
+
133
+ async def _anthropic_stream(
134
+ self, messages, task_id, session_id, temperature, max_tokens
135
+ ) -> str:
136
+ full_text = ""
137
+ system = ""
138
+ filtered = []
139
+ for m in messages:
140
+ if m["role"] == "system":
141
+ system = m["content"]
142
+ else:
143
+ filtered.append(m)
144
+ headers = {
145
+ "x-api-key": ANTHROPIC_API_KEY,
146
+ "anthropic-version": "2023-06-01",
147
+ "Content-Type": "application/json",
148
+ }
149
+ payload = {
150
+ "model": "claude-3-5-sonnet-20241022",
151
+ "max_tokens": max_tokens,
152
+ "messages": filtered,
153
+ "stream": True,
154
+ }
155
+ if system:
156
+ payload["system"] = system
157
+ async with httpx.AsyncClient(timeout=120) as client:
158
+ async with client.stream(
159
+ "POST", "https://api.anthropic.com/v1/messages",
160
+ headers=headers, json=payload
161
+ ) as resp:
162
+ resp.raise_for_status()
163
+ async for line in resp.aiter_lines():
164
+ if not line.startswith("data:"):
165
+ continue
166
+ try:
167
+ data = json.loads(line[5:].strip())
168
+ if data.get("type") == "content_block_delta":
169
+ delta = data["delta"].get("text", "")
170
+ if delta:
171
+ full_text += delta
172
+ if task_id:
173
+ await self.ws.emit(task_id, "llm_chunk", {
174
+ "chunk": delta,
175
+ }, session_id=session_id)
176
+ if session_id and not task_id:
177
+ await self.ws.emit_chat(session_id, "llm_chunk", {
178
+ "chunk": delta,
179
+ })
180
+ except Exception:
181
+ pass
182
+ return full_text
183
+
184
+ async def _demo_stream(self, messages, task_id, session_id) -> str:
185
+ """Demo mode — simulate LLM streaming without API key."""
186
+ last_user = next(
187
+ (m["content"] for m in reversed(messages) if m["role"] == "user"), "Hello"
188
+ )
189
+ response = (
190
+ f"🤖 **Devin Agent** (Demo Mode)\n\n"
191
+ f"I received your request: *{last_user[:100]}*\n\n"
192
+ f"To enable real AI responses, set `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` in your environment.\n\n"
193
+ f"**What I can do with a real API key:**\n"
194
+ f"- 📋 Generate detailed execution plans\n"
195
+ f"- 💻 Write and execute code autonomously\n"
196
+ f"- 🔧 Debug and self-heal on errors\n"
197
+ f"- 🐙 Manage GitHub repos autonomously\n"
198
+ f"- 🧠 Remember long-running project context\n"
199
+ f"- 🚀 Deploy applications automatically\n"
200
+ )
201
+ full_text = ""
202
+ for word in response.split():
203
+ chunk = word + " "
204
+ full_text += chunk
205
+ await asyncio.sleep(0.03)
206
+ if task_id:
207
+ await self.ws.emit(task_id, "llm_chunk", {
208
+ "chunk": chunk,
209
+ "demo": True,
210
+ }, session_id=session_id)
211
+ if session_id and not task_id:
212
+ await self.ws.emit_chat(session_id, "llm_chunk", {
213
+ "chunk": chunk,
214
+ "demo": True,
215
+ })
216
+ return full_text
217
+
218
+ # ─── Planning ──────────────────────────────────────────────────────────────
219
+
220
+ async def plan(self, goal: str, task_id: str, session_id: str = "") -> TaskPlan:
221
+ """Generate a structured execution plan."""
222
+ # Get context from memory
223
+ memories = await search_memory(goal[:50], session_id=session_id)
224
+ context = "\n".join([m["content"][:200] for m in memories[:3]])
225
+
226
+ prompt = PLANNER_PROMPT.format(goal=goal, context=context or "No prior context")
227
+
228
+ messages = [
229
+ {"role": "system", "content": SYSTEM_PROMPT},
230
+ {"role": "user", "content": prompt},
231
+ ]
232
+
233
+ if not OPENAI_API_KEY and not ANTHROPIC_API_KEY:
234
+ # Demo plan
235
+ return self._demo_plan(goal)
236
+
237
+ raw = await self.llm_stream(messages, task_id=task_id, session_id=session_id)
238
+
239
+ # Extract JSON from response
240
+ try:
241
+ # Find JSON block
242
+ start = raw.find("{")
243
+ end = raw.rfind("}") + 1
244
+ if start >= 0 and end > start:
245
+ data = json.loads(raw[start:end])
246
+ else:
247
+ data = json.loads(raw)
248
+
249
+ steps = []
250
+ for i, s in enumerate(data.get("steps", [])):
251
+ steps.append(TaskStep(
252
+ name=s.get("name", f"Step {i+1}"),
253
+ description=s.get("description", ""),
254
+ tool=s.get("tool", "none"),
255
+ ))
256
+
257
+ return TaskPlan(
258
+ goal=goal,
259
+ steps=steps if steps else [TaskStep(name="Execute goal", description=goal, tool="code")],
260
+ estimated_duration=data.get("estimated_duration", 60),
261
+ tools_needed=data.get("tools_needed", []),
262
+ )
263
+ except Exception as e:
264
+ log.warning("Plan parse failed, using fallback", error=str(e))
265
+ return self._demo_plan(goal)
266
+
267
+ def _demo_plan(self, goal: str) -> TaskPlan:
268
+ """Fallback plan for demo mode."""
269
+ steps = [
270
+ TaskStep(name="Analyze Requirements", description=f"Analyze: {goal[:60]}", tool="none"),
271
+ TaskStep(name="Design Solution", description="Design the solution architecture", tool="none"),
272
+ TaskStep(name="Implement", description="Write the implementation code", tool="code"),
273
+ TaskStep(name="Test", description="Test the implementation", tool="test"),
274
+ TaskStep(name="Document", description="Write documentation", tool="none"),
275
+ ]
276
+ return TaskPlan(
277
+ goal=goal,
278
+ steps=steps,
279
+ estimated_duration=120,
280
+ tools_needed=["code", "test"],
281
+ )
282
+
283
+ # ─── Step Execution ────────────────────────────────────────────────────────
284
+
285
+ async def execute_step(
286
+ self,
287
+ step: TaskStep,
288
+ task_id: str,
289
+ session_id: str = "",
290
+ context: Dict = {},
291
+ ) -> str:
292
+ """Execute a single step using the appropriate tool."""
293
+ from tools.executor import ToolExecutor
294
+ executor = ToolExecutor(self.ws)
295
+
296
+ await self.ws.emit(task_id, "tool_called", {
297
+ "tool": step.tool or "none",
298
+ "step": step.name,
299
+ "description": step.description,
300
+ }, session_id=session_id)
301
+
302
+ try:
303
+ result = await executor.run(
304
+ tool=step.tool or "none",
305
+ task=step.description,
306
+ goal=context.get("goal", ""),
307
+ previous=context.get("previous_results", []),
308
+ task_id=task_id,
309
+ session_id=session_id,
310
+ )
311
+ await self.ws.emit(task_id, "tool_result", {
312
+ "tool": step.tool,
313
+ "step": step.name,
314
+ "result": str(result)[:500],
315
+ "success": True,
316
+ }, session_id=session_id)
317
+ return result
318
+ except Exception as e:
319
+ await self.ws.emit(task_id, "tool_result", {
320
+ "tool": step.tool,
321
+ "step": step.name,
322
+ "error": str(e),
323
+ "success": False,
324
+ }, session_id=session_id)
325
+ return f"Error in {step.name}: {str(e)}"
326
+
327
+ # ─── Finalize ──────────────────────────────────────────────────────────────
328
+
329
+ async def finalize(
330
+ self,
331
+ goal: str,
332
+ steps: List[TaskStep],
333
+ results: List[str],
334
+ task_id: str,
335
+ session_id: str = "",
336
+ ) -> str:
337
+ """Compile final result summary."""
338
+ steps_summary = "\n".join([
339
+ f"- {s.name}: {r[:200]}" for s, r in zip(steps, results)
340
+ ])
341
+ messages = [
342
+ {"role": "system", "content": SYSTEM_PROMPT},
343
+ {"role": "user", "content": (
344
+ f"Summarize the completion of this goal:\n"
345
+ f"Goal: {goal}\n\n"
346
+ f"Steps completed:\n{steps_summary}\n\n"
347
+ f"Write a concise success summary with key outcomes."
348
+ )},
349
+ ]
350
+ result = await self.llm_stream(messages, task_id=task_id, session_id=session_id)
351
+ return result or f"✅ Completed: {goal}"
352
+
353
+ # ─── Chat ──────────────────────────────────────────────────────────────────
354
+
355
+ async def stream_chat(self, session_id: str, user_message: str):
356
+ """Stream a conversational chat response."""
357
+ # Save user message to memory
358
+ await save_memory(
359
+ content=user_message,
360
+ memory_type="conversation",
361
+ session_id=session_id,
362
+ key="user_message",
363
+ )
364
+
365
+ # Get conversation history
366
+ history = await get_history(session_id, limit=10)
367
+ messages = [{"role": "system", "content": SYSTEM_PROMPT}]
368
+ for h in reversed(history[-10:]):
369
+ messages.append({"role": "user", "content": h["content"]})
370
+
371
+ messages.append({"role": "user", "content": user_message})
372
+
373
+ await self.ws.emit_chat(session_id, "stream_start", {
374
+ "status": "generating",
375
+ })
376
+
377
+ response = await self.llm_stream(messages, session_id=session_id)
378
+
379
+ # Save assistant response to memory
380
+ await save_memory(
381
+ content=response,
382
+ memory_type="conversation",
383
+ session_id=session_id,
384
+ key="assistant_response",
385
+ )
386
+
387
+ await self.ws.emit_chat(session_id, "stream_end", {
388
+ "full_response": response,
389
+ "status": "complete",
390
+ })
391
+
392
+ return response
core/database.py DELETED
@@ -1,132 +0,0 @@
1
- import sqlite3
2
- import json
3
- import os
4
- from datetime import datetime
5
- from typing import Any, Dict, List, Optional
6
-
7
- DB_PATH = os.getenv("DB_PATH", "autonomous_coding.db")
8
-
9
- class Database:
10
- def __init__(self):
11
- self.conn = sqlite3.connect(DB_PATH, check_same_thread=False)
12
- self.conn.row_factory = sqlite3.Row
13
- self.create_tables()
14
-
15
- def create_tables(self):
16
- cursor = self.conn.cursor()
17
-
18
- # Tasks table
19
- cursor.execute('''
20
- CREATE TABLE IF NOT EXISTS tasks (
21
- id TEXT PRIMARY KEY,
22
- goal TEXT,
23
- type TEXT,
24
- status TEXT,
25
- progress INTEGER,
26
- result TEXT,
27
- error TEXT,
28
- created_at TIMESTAMP,
29
- updated_at TIMESTAMP
30
- )
31
- ''')
32
-
33
- # Memory table
34
- cursor.execute('''
35
- CREATE TABLE IF NOT EXISTS memory (
36
- id INTEGER PRIMARY KEY AUTOINCREMENT,
37
- project_id TEXT,
38
- category TEXT, -- goal, plan, execution, tool, error, file_state
39
- content TEXT,
40
- timestamp TIMESTAMP
41
- )
42
- ''')
43
-
44
- # Logs table
45
- cursor.execute('''
46
- CREATE TABLE IF NOT EXISTS logs (
47
- id INTEGER PRIMARY KEY AUTOINCREMENT,
48
- task_id TEXT,
49
- message TEXT,
50
- timestamp TIMESTAMP,
51
- FOREIGN KEY (task_id) REFERENCES tasks (id)
52
- )
53
- ''')
54
-
55
- self.conn.commit()
56
-
57
- def save_task(self, task_data: Dict[str, Any]):
58
- cursor = self.conn.cursor()
59
- cursor.execute('''
60
- INSERT OR REPLACE INTO tasks (id, goal, type, status, progress, result, error, created_at, updated_at)
61
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
62
- ''', (
63
- task_data['id'],
64
- task_data['goal'],
65
- task_data['type'],
66
- task_data['status'],
67
- task_data.get('progress', 0),
68
- json.dumps(task_data.get('result')),
69
- task_data.get('error'),
70
- task_data['created_at'],
71
- task_data['updated_at']
72
- ))
73
- self.conn.commit()
74
-
75
- def get_task(self, task_id: str) -> Optional[Dict[str, Any]]:
76
- cursor = self.conn.cursor()
77
- cursor.execute('SELECT * FROM tasks WHERE id = ?', (task_id,))
78
- row = cursor.fetchone()
79
- if row:
80
- data = dict(row)
81
- data['result'] = json.loads(data['result']) if data['result'] else None
82
- return data
83
- return None
84
-
85
- def list_tasks(self) -> List[Dict[str, Any]]:
86
- cursor = self.conn.cursor()
87
- cursor.execute('SELECT * FROM tasks ORDER BY created_at DESC')
88
- rows = cursor.fetchall()
89
- tasks = []
90
- for row in rows:
91
- data = dict(row)
92
- data['result'] = json.loads(data['result']) if data['result'] else None
93
- tasks.append(data)
94
- return tasks
95
-
96
- def add_memory(self, project_id: str, category: str, content: Any):
97
- cursor = self.conn.cursor()
98
- cursor.execute('''
99
- INSERT INTO memory (project_id, category, content, timestamp)
100
- VALUES (?, ?, ?, ?)
101
- ''', (project_id, category, json.dumps(content), datetime.utcnow().isoformat()))
102
- self.conn.commit()
103
-
104
- def get_memory(self, project_id: str, category: Optional[str] = None) -> List[Dict[str, Any]]:
105
- cursor = self.conn.cursor()
106
- if category:
107
- cursor.execute('SELECT * FROM memory WHERE project_id = ? AND category = ? ORDER BY timestamp DESC', (project_id, category))
108
- else:
109
- cursor.execute('SELECT * FROM memory WHERE project_id = ? ORDER BY timestamp DESC', (project_id,))
110
-
111
- rows = cursor.fetchall()
112
- memories = []
113
- for row in rows:
114
- data = dict(row)
115
- data['content'] = json.loads(data['content'])
116
- memories.append(data)
117
- return memories
118
-
119
- def add_log(self, task_id: str, message: str):
120
- cursor = self.conn.cursor()
121
- cursor.execute('''
122
- INSERT INTO logs (task_id, message, timestamp)
123
- VALUES (?, ?, ?)
124
- ''', (task_id, message, datetime.utcnow().isoformat()))
125
- self.conn.commit()
126
-
127
- def get_logs(self, task_id: str) -> List[Dict[str, Any]]:
128
- cursor = self.conn.cursor()
129
- cursor.execute('SELECT * FROM logs WHERE task_id = ? ORDER BY timestamp ASC', (task_id,))
130
- return [dict(row) for row in cursor.fetchall()]
131
-
132
- db = Database()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/github_engine.py DELETED
@@ -1,80 +0,0 @@
1
- import os
2
- import subprocess
3
- import requests
4
- from typing import Optional, List, Dict, Any
5
-
6
- class GitHubEngine:
7
- def __init__(self, token: Optional[str] = None):
8
- self.token = token or os.getenv("GITHUB_TOKEN")
9
- self.api_base = "https://api.github.com"
10
- self.headers = {
11
- "Authorization": f"token {self.token}",
12
- "Accept": "application/vnd.github.v3+json"
13
- } if self.token else {}
14
-
15
- def _run_git(self, args: List[str], cwd: Optional[str] = None) -> str:
16
- try:
17
- result = subprocess.run(
18
- ["git"] + args,
19
- cwd=cwd,
20
- capture_output=True,
21
- text=True,
22
- check=True
23
- )
24
- return result.stdout.strip()
25
- except subprocess.CalledProcessError as e:
26
- raise Exception(f"Git command failed: {e.stderr}")
27
-
28
- def clone(self, repo_url: str, dest_path: str) -> str:
29
- if self.token and "github.com" in repo_url:
30
- auth_url = repo_url.replace("https://", f"https://x-access-token:{self.token}@")
31
- else:
32
- auth_url = repo_url
33
- return self._run_git(["clone", auth_url, dest_path])
34
-
35
- def create_repo(self, name: str, private: bool = True) -> Dict[str, Any]:
36
- resp = requests.post(
37
- f"{self.api_base}/user/repos",
38
- headers=self.headers,
39
- json={"name": name, "private": private}
40
- )
41
- resp.raise_for_status()
42
- return resp.json()
43
-
44
- def manage_branch(self, repo_path: str, branch_name: str, create: bool = False):
45
- args = ["checkout", "-b" if create else "", branch_name]
46
- args = [a for a in args if a]
47
- return self._run_git(args, cwd=repo_path)
48
-
49
- def commit_and_push(self, repo_path: str, message: str, branch: str = "main"):
50
- self._run_git(["add", "."], cwd=repo_path)
51
- self._run_git(["commit", "-m", message], cwd=repo_path)
52
- self._run_git(["push", "origin", branch], cwd=repo_path)
53
-
54
- def create_pr(self, repo_full_name: str, title: str, body: str, head: str, base: str = "main") -> Dict[str, Any]:
55
- resp = requests.post(
56
- f"{self.api_base}/repos/{repo_full_name}/pulls",
57
- headers=self.headers,
58
- json={"title": title, "body": body, "head": head, "base": base}
59
- )
60
- resp.raise_for_status()
61
- return resp.json()
62
-
63
- def list_issues(self, repo_full_name: str, state: str = "open") -> List[Dict[str, Any]]:
64
- resp = requests.get(
65
- f"{self.api_base}/repos/{repo_full_name}/issues",
66
- headers=self.headers,
67
- params={"state": state}
68
- )
69
- resp.raise_for_status()
70
- return resp.json()
71
-
72
- def get_workflow_runs(self, repo_full_name: str) -> List[Dict[str, Any]]:
73
- resp = requests.get(
74
- f"{self.api_base}/repos/{repo_full_name}/actions/runs",
75
- headers=self.headers
76
- )
77
- resp.raise_for_status()
78
- return resp.json().get("workflow_runs", [])
79
-
80
- github_engine = GitHubEngine()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/llm_router.py DELETED
@@ -1,37 +0,0 @@
1
- import os
2
- import aiohttp
3
- import json
4
- from typing import List, Dict, Any, Optional
5
-
6
- class LLMRouter:
7
- def __init__(self):
8
- self.gateway_url = "https://gateway.pyaesone-gtckglay.workers.dev/v1/chat/completions"
9
- # API Key is already included in the gateway URL as per user instruction,
10
- # but we'll allow an override via environment variable if needed.
11
- self.api_key = os.getenv("LLM_API_KEY", "")
12
-
13
- async def chat_completion(self, messages: List[Dict[str, str]], model: str = "gpt-4", stream: bool = False) -> Dict[str, Any]:
14
- headers = {
15
- "Content-Type": "application/json"
16
- }
17
- if self.api_key:
18
- headers["Authorization"] = f"Bearer {self.api_key}"
19
-
20
- payload = {
21
- "model": model,
22
- "messages": messages,
23
- "stream": stream
24
- }
25
-
26
- async with aiohttp.ClientSession() as session:
27
- async with session.post(self.gateway_url, headers=headers, json=payload) as response:
28
- if response.status != 200:
29
- error_text = await response.text()
30
- raise Exception(f"LLM Gateway Error ({response.status}): {error_text}")
31
-
32
- if stream:
33
- return response # Return the response object for streaming
34
-
35
- return await response.json()
36
-
37
- llm_router = LLMRouter()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/memory.py DELETED
@@ -1,44 +0,0 @@
1
- from typing import Any, Dict, List, Optional
2
- from core.database import db
3
-
4
- class MemoryEngine:
5
- def __init__(self, project_id: str = "default"):
6
- self.project_id = project_id
7
-
8
- def save_goal(self, goal: str):
9
- db.add_memory(self.project_id, "goal", goal)
10
-
11
- def save_plan(self, plan: List[Dict[str, Any]]):
12
- db.add_memory(self.project_id, "plan", plan)
13
-
14
- def save_execution(self, step: str, result: Any):
15
- db.add_memory(self.project_id, "execution", {"step": step, "result": result})
16
-
17
- def save_tool_usage(self, tool_name: str, args: Dict[str, Any], output: Any):
18
- db.add_memory(self.project_id, "tool", {"tool": tool_name, "args": args, "output": output})
19
-
20
- def save_error(self, error: str, context: Optional[str] = None):
21
- db.add_memory(self.project_id, "error", {"error": error, "context": context})
22
-
23
- def save_file_state(self, file_path: str, checksum: str):
24
- db.add_memory(self.project_id, "file_state", {"path": file_path, "checksum": checksum})
25
-
26
- def get_full_context(self) -> str:
27
- memories = db.get_memory(self.project_id)
28
- if not memories:
29
- return "No previous memory found for this project."
30
-
31
- context_parts = ["### Project Memory Context:"]
32
- for m in reversed(memories): # Oldest to newest
33
- category = m['category'].upper()
34
- content = m['content']
35
- timestamp = m['timestamp']
36
- context_parts.append(f"[{timestamp}] {category}: {content}")
37
-
38
- return "\n".join(context_parts)
39
-
40
- def get_recent_memories(self, limit: int = 10) -> List[Dict[str, Any]]:
41
- memories = db.get_memory(self.project_id)
42
- return memories[:limit]
43
-
44
- memory_engine = MemoryEngine()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/models.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pydantic Models — Task, Chat, Memory, GitHub
3
+ """
4
+
5
+ import time
6
+ import uuid
7
+ from enum import Enum
8
+ from typing import Any, Dict, List, Optional
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ def gen_id(prefix: str = "") -> str:
13
+ return f"{prefix}{uuid.uuid4().hex[:12]}"
14
+
15
+
16
+ # ─── Enums ─────────────────────────────────────────────────────────────────────
17
+
18
+ class TaskStatus(str, Enum):
19
+ queued = "queued"
20
+ initializing = "initializing"
21
+ planning = "planning"
22
+ executing = "executing"
23
+ streaming = "streaming"
24
+ waiting_input = "waiting_input"
25
+ retrying = "retrying"
26
+ finalizing = "finalizing"
27
+ completed = "completed"
28
+ failed = "failed"
29
+ cancelled = "cancelled"
30
+
31
+
32
+ class EventType(str, Enum):
33
+ task_created = "task_created"
34
+ task_queued = "task_queued"
35
+ task_started = "task_started"
36
+ plan_generated = "plan_generated"
37
+ step_started = "step_started"
38
+ step_progress = "step_progress"
39
+ tool_called = "tool_called"
40
+ tool_result = "tool_result"
41
+ llm_chunk = "llm_chunk"
42
+ memory_updated = "memory_updated"
43
+ retry_attempt = "retry_attempt"
44
+ step_completed = "step_completed"
45
+ warning = "warning"
46
+ error = "error"
47
+ task_completed = "task_completed"
48
+ task_failed = "task_failed"
49
+ heartbeat = "heartbeat"
50
+
51
+
52
+ class MemoryType(str, Enum):
53
+ conversation = "conversation"
54
+ task = "task"
55
+ project = "project"
56
+ execution = "execution"
57
+ tool = "tool"
58
+ error = "error"
59
+ repo = "repo"
60
+ planning = "planning"
61
+
62
+
63
+ # ─── Task Models ───────────────────────────────────────────────────────────────
64
+
65
+ class TaskCreateRequest(BaseModel):
66
+ goal: str = Field(..., min_length=1, max_length=10000, description="What should the agent do?")
67
+ session_id: str = Field(default_factory=lambda: gen_id("sess_"))
68
+ project_id: str = Field(default="")
69
+ stream: bool = True
70
+ metadata: Dict[str, Any] = Field(default_factory=dict)
71
+ github_repo: Optional[str] = None
72
+ auto_commit: bool = False
73
+
74
+
75
+ class TaskStep(BaseModel):
76
+ id: str = Field(default_factory=lambda: gen_id("step_"))
77
+ name: str
78
+ description: str = ""
79
+ tool: Optional[str] = None
80
+ status: str = "pending"
81
+ output: Optional[str] = None
82
+ error: Optional[str] = None
83
+ started_at: Optional[float] = None
84
+ completed_at: Optional[float] = None
85
+ duration_ms: Optional[float] = None
86
+
87
+
88
+ class TaskPlan(BaseModel):
89
+ goal: str
90
+ steps: List[TaskStep]
91
+ estimated_duration: int = 0
92
+ tools_needed: List[str] = []
93
+ created_at: float = Field(default_factory=time.time)
94
+
95
+
96
+ class TaskResponse(BaseModel):
97
+ id: str
98
+ goal: str
99
+ status: TaskStatus
100
+ session_id: str
101
+ project_id: str
102
+ plan: Optional[TaskPlan] = None
103
+ result: Optional[str] = None
104
+ error: Optional[str] = None
105
+ created_at: float
106
+ started_at: Optional[float] = None
107
+ completed_at: Optional[float] = None
108
+ retry_count: int = 0
109
+ stream_url: Optional[str] = None
110
+ ws_url: Optional[str] = None
111
+
112
+
113
+ class TaskCancelRequest(BaseModel):
114
+ reason: str = "User cancelled"
115
+
116
+
117
+ class TaskRetryRequest(BaseModel):
118
+ reset_state: bool = True
119
+
120
+
121
+ # ─── Chat Models ───────────────────────────────────────────────────────────────
122
+
123
+ class ChatMessage(BaseModel):
124
+ role: str = Field(..., pattern="^(user|assistant|system)$")
125
+ content: str
126
+ timestamp: float = Field(default_factory=time.time)
127
+
128
+
129
+ class ChatRequest(BaseModel):
130
+ messages: List[ChatMessage]
131
+ session_id: str = Field(default_factory=lambda: gen_id("sess_"))
132
+ project_id: str = ""
133
+ stream: bool = True
134
+ model: str = "gpt-4o"
135
+ temperature: float = 0.7
136
+ max_tokens: int = 4096
137
+ system_prompt: Optional[str] = None
138
+
139
+
140
+ class GoalRequest(BaseModel):
141
+ goal: str = Field(..., min_length=1, max_length=10000)
142
+ session_id: str = Field(default_factory=lambda: gen_id("sess_"))
143
+ project_id: str = ""
144
+ stream: bool = True
145
+ auto_execute: bool = True
146
+ github_repo: Optional[str] = None
147
+
148
+
149
+ # ─── Memory Models ─────────────────────────────────────────────────────────────
150
+
151
+ class MemorySaveRequest(BaseModel):
152
+ content: str
153
+ memory_type: MemoryType
154
+ session_id: str = ""
155
+ project_id: str = ""
156
+ key: str = ""
157
+ metadata: Dict[str, Any] = {}
158
+
159
+
160
+ class MemorySearchRequest(BaseModel):
161
+ query: str
162
+ session_id: str = ""
163
+ project_id: str = ""
164
+ limit: int = 20
165
+
166
+
167
+ # ─── GitHub Models ───────────────��─────────────────────────────────────────────
168
+
169
+ class GitHubCloneRequest(BaseModel):
170
+ repo_url: str
171
+ branch: str = "main"
172
+ local_path: Optional[str] = None
173
+
174
+
175
+ class GitHubCreateRepoRequest(BaseModel):
176
+ name: str
177
+ description: str = ""
178
+ private: bool = False
179
+ auto_init: bool = True
180
+
181
+
182
+ class GitHubCommitRequest(BaseModel):
183
+ repo: str
184
+ branch: str = "main"
185
+ files: Dict[str, str] # path → content
186
+ message: str
187
+ create_branch: bool = False
188
+
189
+
190
+ class GitHubPRRequest(BaseModel):
191
+ repo: str
192
+ title: str
193
+ body: str = ""
194
+ head: str
195
+ base: str = "main"
196
+ draft: bool = False
197
+
198
+
199
+ class GitHubIssueRequest(BaseModel):
200
+ repo: str
201
+ title: str
202
+ body: str = ""
203
+ labels: List[str] = []
204
+
205
+
206
+ # ─── Event Schema (unified) ────────────────────────────────────────────────────
207
+
208
+ class StreamEvent(BaseModel):
209
+ type: str
210
+ task_id: str = ""
211
+ session_id: str = ""
212
+ timestamp: float = Field(default_factory=time.time)
213
+ data: Dict[str, Any] = {}
core/orchestrator.py DELETED
@@ -1,96 +0,0 @@
1
- import json
2
- import asyncio
3
- from typing import List, Dict, Any, Optional
4
- from core.llm_router import llm_router
5
- from core.memory import memory_engine
6
- from task_manager import task_manager, TaskStatus
7
-
8
- class ToolRegistry:
9
- def __init__(self):
10
- self.tools = {
11
- "github.clone": {"purpose": "Clone a GitHub repository", "params": ["url", "path"]},
12
- "github.create": {"purpose": "Create a new GitHub repository", "params": ["name", "private"]},
13
- "github.commit_push": {"purpose": "Commit and push changes", "params": ["path", "message", "branch"]},
14
- "workspace.read": {"purpose": "Read a file from workspace", "params": ["path"]},
15
- "workspace.write": {"purpose": "Write content to a file", "params": ["path", "content"]},
16
- "workspace.list": {"purpose": "List files in a directory", "params": ["path"]},
17
- "browser.open": {"purpose": "Open a URL in browser", "params": ["url"]},
18
- }
19
-
20
- def get_metadata(self) -> str:
21
- return json.dumps(self.tools, indent=2)
22
-
23
- class Orchestrator:
24
- def __init__(self):
25
- self.registry = ToolRegistry()
26
-
27
- async def plan(self, goal: str) -> List[Dict[str, Any]]:
28
- context = memory_engine.get_full_context()
29
- tools_info = self.registry.get_metadata()
30
-
31
- prompt = f"""
32
- Goal: {goal}
33
-
34
- Available Tools:
35
- {tools_info}
36
-
37
- Previous Context:
38
- {context}
39
-
40
- Task: Create a step-by-step plan to achieve the goal using the available tools.
41
- Return the plan as a JSON list of objects: [{{"step": 1, "tool": "tool.name", "args": {{...}}, "description": "..."}}]
42
- """
43
-
44
- messages = [{"role": "system", "content": "You are an expert software engineer planner."},
45
- {"role": "user", "content": prompt}]
46
-
47
- response = await llm_router.chat_completion(messages)
48
- content = response['choices'][0]['message']['content']
49
-
50
- # Extract JSON from response
51
- try:
52
- plan = json.loads(content[content.find('['):content.rfind(']')+1])
53
- return plan
54
- except:
55
- # Fallback or retry logic
56
- return []
57
-
58
- async def execute_task(self, task_id: str):
59
- task = task_manager.get_task(task_id)
60
- if not task: return
61
-
62
- task.update(status=TaskStatus.RUNNING, progress=10)
63
- memory_engine.save_goal(task.goal)
64
-
65
- plan = await self.plan(task.goal)
66
- if not plan:
67
- task.update(status=TaskStatus.FAILED, error="Failed to generate plan")
68
- return
69
-
70
- memory_engine.save_plan(plan)
71
- task.add_log(f"Plan generated: {len(plan)} steps")
72
-
73
- for i, step in enumerate(plan):
74
- task.add_log(f"Executing step {step['step']}: {step['description']}")
75
-
76
- # In a real system, this would call the actual tool implementation
77
- # For now, we simulate execution and update memory
78
- try:
79
- # Simulate tool call
80
- result = {"status": "success", "step": step['step']}
81
- memory_engine.save_tool_usage(step['tool'], step['args'], result)
82
- memory_engine.save_execution(step['description'], result)
83
-
84
- progress = int(10 + (i + 1) / len(plan) * 80)
85
- task.update(progress=progress)
86
- except Exception as e:
87
- task.add_log(f"Error in step {step['step']}: {str(e)}")
88
- memory_engine.save_error(str(e), f"Step {step['step']}")
89
- # Self-healing: Could re-plan here
90
- task.update(status=TaskStatus.FAILED, error=str(e))
91
- return
92
-
93
- task.update(status=TaskStatus.COMPLETED, progress=100, result="Goal achieved successfully")
94
- task.add_log("Task completed successfully")
95
-
96
- orchestrator = Orchestrator()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/task_engine.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Task Engine — Heart of the Autonomous Agent
3
+ Manages task lifecycle, planning, execution, self-healing
4
+ """
5
+
6
+ import asyncio
7
+ import json
8
+ import os
9
+ import time
10
+ import uuid
11
+ from typing import Dict, Optional, List
12
+
13
+ import structlog
14
+
15
+ from core.models import TaskStatus, TaskPlan, TaskStep, TaskCreateRequest
16
+ from api.websocket_manager import WebSocketManager
17
+ from memory.db import (
18
+ create_task, update_task_status, get_task, save_task_event,
19
+ save_memory, get_task_events
20
+ )
21
+
22
+ log = structlog.get_logger()
23
+
24
+ MAX_RETRIES = 3
25
+ MAX_CONCURRENT = 5
26
+
27
+
28
+ class TaskEngine:
29
+ def __init__(self, ws_manager: WebSocketManager):
30
+ self.ws = ws_manager
31
+ self._queue: asyncio.Queue = asyncio.Queue()
32
+ self._active: Dict[str, asyncio.Task] = {}
33
+ self._running = False
34
+ self._workers: List[asyncio.Task] = []
35
+
36
+ async def start(self):
37
+ self._running = True
38
+ for i in range(MAX_CONCURRENT):
39
+ worker = asyncio.create_task(self._worker(i))
40
+ self._workers.append(worker)
41
+ log.info("TaskEngine started", workers=MAX_CONCURRENT)
42
+
43
+ async def stop(self):
44
+ self._running = False
45
+ for w in self._workers:
46
+ w.cancel()
47
+ log.info("TaskEngine stopped")
48
+
49
+ # ─── Public API ────────────────────────────────────────────────────────────
50
+
51
+ async def submit(self, req: TaskCreateRequest) -> str:
52
+ task_id = f"task_{uuid.uuid4().hex[:10]}"
53
+ await create_task(
54
+ task_id=task_id,
55
+ goal=req.goal,
56
+ session_id=req.session_id,
57
+ project_id=req.project_id,
58
+ metadata={**req.metadata, "github_repo": req.github_repo, "auto_commit": req.auto_commit},
59
+ )
60
+ await self.ws.emit(task_id, "task_created", {
61
+ "goal": req.goal,
62
+ "session_id": req.session_id,
63
+ "stream_url": f"/api/v1/tasks/{task_id}/stream",
64
+ "ws_url": f"/ws/tasks/{task_id}",
65
+ }, session_id=req.session_id)
66
+ await self._queue.put((task_id, req))
67
+ await self.ws.emit(task_id, "task_queued", {
68
+ "position": self._queue.qsize(),
69
+ }, session_id=req.session_id)
70
+ log.info("Task submitted", task_id=task_id, goal=req.goal[:60])
71
+ return task_id
72
+
73
+ async def cancel(self, task_id: str, reason: str = "User cancelled"):
74
+ if task_id in self._active:
75
+ self._active[task_id].cancel()
76
+ del self._active[task_id]
77
+ await update_task_status(task_id, "cancelled", error=reason)
78
+ await self.ws.emit(task_id, "task_failed", {"reason": reason, "status": "cancelled"})
79
+
80
+ async def retry(self, task_id: str):
81
+ task = await get_task(task_id)
82
+ if not task:
83
+ return
84
+ req = TaskCreateRequest(
85
+ goal=task["goal"],
86
+ session_id=task["session_id"] or "",
87
+ project_id=task["project_id"] or "",
88
+ metadata=task.get("metadata") or {},
89
+ )
90
+ retry_count = (task.get("retry_count") or 0) + 1
91
+ await update_task_status(task_id, "queued", retry_count=retry_count)
92
+ await self.ws.emit(task_id, "retry_attempt", {"count": retry_count})
93
+ await self._queue.put((task_id, req))
94
+
95
+ async def handle_chat_message(self, session_id: str, content: str, websocket=None):
96
+ """Handle real-time chat message with streaming response."""
97
+ from core.agent import AgentCore
98
+ agent = AgentCore(self.ws)
99
+ await agent.stream_chat(session_id=session_id, user_message=content)
100
+
101
+ # ─── Worker Loop ───────────────────────────────────────────────────────────
102
+
103
+ async def _worker(self, worker_id: int):
104
+ log.info(f"Worker {worker_id} started")
105
+ while self._running:
106
+ try:
107
+ task_id, req = await asyncio.wait_for(self._queue.get(), timeout=1.0)
108
+ worker_task = asyncio.create_task(self._execute(task_id, req))
109
+ self._active[task_id] = worker_task
110
+ await worker_task
111
+ self._active.pop(task_id, None)
112
+ self._queue.task_done()
113
+ except asyncio.TimeoutError:
114
+ continue
115
+ except asyncio.CancelledError:
116
+ break
117
+ except Exception as e:
118
+ log.error(f"Worker {worker_id} error", error=str(e))
119
+
120
+ async def _execute(self, task_id: str, req: TaskCreateRequest):
121
+ """Full task execution lifecycle."""
122
+ from core.agent import AgentCore
123
+ agent = AgentCore(self.ws)
124
+
125
+ try:
126
+ # ── Initializing ────────────────────────────────────────────────
127
+ await update_task_status(task_id, "initializing")
128
+ await self.ws.emit(task_id, "task_started", {
129
+ "goal": req.goal,
130
+ "status": "initializing",
131
+ }, session_id=req.session_id)
132
+ await save_task_event(task_id, "task_started", {"goal": req.goal})
133
+
134
+ # ── Planning ────────────────────────────────────────────────────
135
+ await update_task_status(task_id, "planning")
136
+ await self.ws.emit(task_id, "step_started", {
137
+ "step": "Planning",
138
+ "status": "planning",
139
+ "description": "Generating execution plan...",
140
+ }, session_id=req.session_id)
141
+
142
+ plan = await agent.plan(goal=req.goal, task_id=task_id, session_id=req.session_id)
143
+
144
+ await update_task_status(task_id, "executing", plan=plan.model_dump())
145
+ await self.ws.emit(task_id, "plan_generated", {
146
+ "steps": [s.model_dump() for s in plan.steps],
147
+ "estimated_duration": plan.estimated_duration,
148
+ "tools_needed": plan.tools_needed,
149
+ }, session_id=req.session_id)
150
+ await save_task_event(task_id, "plan_generated", {"steps_count": len(plan.steps)})
151
+
152
+ # ── Execute Steps ────────────────────────────────────────────────
153
+ results = []
154
+ for i, step in enumerate(plan.steps):
155
+ await self.ws.emit(task_id, "step_started", {
156
+ "step": step.name,
157
+ "step_id": step.id,
158
+ "index": i,
159
+ "total": len(plan.steps),
160
+ "tool": step.tool,
161
+ }, session_id=req.session_id)
162
+
163
+ step_result = await agent.execute_step(
164
+ step=step,
165
+ task_id=task_id,
166
+ session_id=req.session_id,
167
+ context={"goal": req.goal, "previous_results": results},
168
+ )
169
+ results.append(step_result)
170
+
171
+ await self.ws.emit(task_id, "step_completed", {
172
+ "step": step.name,
173
+ "step_id": step.id,
174
+ "index": i,
175
+ "output": step_result[:500] if isinstance(step_result, str) else str(step_result)[:500],
176
+ "status": "completed",
177
+ }, session_id=req.session_id)
178
+ await save_task_event(task_id, "step_completed", {"step": step.name, "index": i})
179
+
180
+ # ── Finalize ─────────────────────────────────────────────────────
181
+ await update_task_status(task_id, "finalizing")
182
+ await self.ws.emit(task_id, "step_started", {
183
+ "step": "Finalizing",
184
+ "description": "Compiling results...",
185
+ }, session_id=req.session_id)
186
+
187
+ final_result = await agent.finalize(
188
+ goal=req.goal,
189
+ steps=plan.steps,
190
+ results=results,
191
+ task_id=task_id,
192
+ session_id=req.session_id,
193
+ )
194
+
195
+ await update_task_status(task_id, "completed", result=final_result)
196
+ await self.ws.emit(task_id, "task_completed", {
197
+ "result": final_result,
198
+ "steps_completed": len(plan.steps),
199
+ "duration": time.time(),
200
+ }, session_id=req.session_id)
201
+
202
+ # Save to memory
203
+ await save_memory(
204
+ content=f"Task: {req.goal}\nResult: {final_result}",
205
+ memory_type="task",
206
+ session_id=req.session_id,
207
+ project_id=req.project_id,
208
+ key=task_id,
209
+ )
210
+ await self.ws.emit(task_id, "memory_updated", {
211
+ "type": "task",
212
+ "key": task_id,
213
+ }, session_id=req.session_id)
214
+
215
+ log.info("Task completed", task_id=task_id)
216
+
217
+ except asyncio.CancelledError:
218
+ await update_task_status(task_id, "cancelled")
219
+ await self.ws.emit(task_id, "task_failed", {"reason": "cancelled"})
220
+ except Exception as e:
221
+ log.error("Task failed", task_id=task_id, error=str(e))
222
+ task_data = await get_task(task_id)
223
+ retry_count = (task_data or {}).get("retry_count", 0)
224
+
225
+ await self.ws.emit(task_id, "error", {
226
+ "error": str(e),
227
+ "retry_count": retry_count,
228
+ "will_retry": retry_count < MAX_RETRIES,
229
+ }, session_id=req.session_id)
230
+
231
+ if retry_count < MAX_RETRIES:
232
+ await update_task_status(task_id, "retrying", retry_count=retry_count + 1)
233
+ await asyncio.sleep(2 ** retry_count)
234
+ await self.ws.emit(task_id, "retry_attempt", {"count": retry_count + 1})
235
+ await self._execute(task_id, req)
236
+ else:
237
+ await update_task_status(task_id, "failed", error=str(e))
238
+ await self.ws.emit(task_id, "task_failed", {
239
+ "error": str(e),
240
+ "retry_count": retry_count,
241
+ }, session_id=req.session_id)
ecosystem.config.cjs ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ apps: [
3
+ {
4
+ name: 'devin-backend',
5
+ script: 'uvicorn',
6
+ args: 'main:app --host 0.0.0.0 --port 7860 --loop asyncio --log-level info',
7
+ interpreter: 'python3',
8
+ cwd: '/home/user/devin-agent/backend',
9
+ watch: false,
10
+ instances: 1,
11
+ exec_mode: 'fork',
12
+ env: {
13
+ PORT: 7860,
14
+ HOST: '0.0.0.0',
15
+ DB_PATH: '/tmp/devin_agent.db',
16
+ PYTHONUNBUFFERED: '1',
17
+ },
18
+ },
19
+ ],
20
+ }
root/etc/s6-overlay/s6-rc.d/init-openvscode-server/dependencies.d/init-config → github/__init__.py RENAMED
File without changes
main.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 🚀 Devin-Style Autonomous AI Engineering Platform
3
+ Production-Grade FastAPI + WebSocket Backend
4
+ """
5
+
6
+ import asyncio
7
+ import json
8
+ import logging
9
+ import os
10
+ import time
11
+ import uuid
12
+ from contextlib import asynccontextmanager
13
+ from typing import Optional
14
+
15
+ import structlog
16
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Depends, Request
17
+ from fastapi.middleware.cors import CORSMiddleware
18
+ from fastapi.middleware.gzip import GZipMiddleware
19
+ from fastapi.responses import JSONResponse
20
+ from slowapi import Limiter, _rate_limit_exceeded_handler
21
+ from slowapi.util import get_remote_address
22
+ from slowapi.errors import RateLimitExceeded
23
+
24
+ from api.routes import tasks, chat, memory, github, health
25
+ from api.websocket_manager import WebSocketManager
26
+ from core.task_engine import TaskEngine
27
+ from memory.db import init_db
28
+
29
+ # ─── Structured Logging ────────────────────────────────────────────────────────
30
+ structlog.configure(
31
+ processors=[
32
+ structlog.processors.TimeStamper(fmt="iso"),
33
+ structlog.stdlib.add_log_level,
34
+ structlog.processors.StackInfoRenderer(),
35
+ structlog.dev.ConsoleRenderer(),
36
+ ]
37
+ )
38
+ log = structlog.get_logger()
39
+
40
+ # ─── Rate Limiter ──────────────────────────────────────────────────────────────
41
+ limiter = Limiter(key_func=get_remote_address)
42
+
43
+ # ─── Global Managers (shared state) ───────────────────────────────────────────
44
+ ws_manager = WebSocketManager()
45
+ task_engine = TaskEngine(ws_manager)
46
+
47
+
48
+ @asynccontextmanager
49
+ async def lifespan(app: FastAPI):
50
+ """Startup + Shutdown lifecycle."""
51
+ log.info("🚀 Starting Devin Agent Platform...")
52
+ await init_db()
53
+ await task_engine.start()
54
+ asyncio.create_task(ws_manager.heartbeat_loop())
55
+ log.info("✅ Platform ready")
56
+ yield
57
+ log.info("🛑 Shutting down...")
58
+ await task_engine.stop()
59
+ log.info("✅ Shutdown complete")
60
+
61
+
62
+ # ─── FastAPI App ───────────────────────────────────────────────────────────────
63
+ app = FastAPI(
64
+ title="🤖 Devin Agent Platform",
65
+ description="Production-Grade Autonomous AI Engineering Platform",
66
+ version="2.0.0",
67
+ lifespan=lifespan,
68
+ docs_url="/api/docs",
69
+ redoc_url="/api/redoc",
70
+ )
71
+
72
+ app.state.limiter = limiter
73
+ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
74
+
75
+ # ─── Middleware ────────────────────────────────────────────────────────────────
76
+ app.add_middleware(
77
+ CORSMiddleware,
78
+ allow_origins=["*"],
79
+ allow_credentials=True,
80
+ allow_methods=["*"],
81
+ allow_headers=["*"],
82
+ )
83
+ app.add_middleware(GZipMiddleware, minimum_size=1000)
84
+
85
+
86
+ # ─── Request Logging ───────────────────────────────────────────────────────────
87
+ @app.middleware("http")
88
+ async def log_requests(request: Request, call_next):
89
+ start = time.time()
90
+ response = await call_next(request)
91
+ duration = round((time.time() - start) * 1000, 2)
92
+ log.info("HTTP", method=request.method, path=request.url.path, status=response.status_code, ms=duration)
93
+ return response
94
+
95
+
96
+ # ─── Inject shared state into routes ──────────────────────────────────────────
97
+ app.state.ws_manager = ws_manager
98
+ app.state.task_engine = task_engine
99
+
100
+
101
+ # ─── REST API Routers ──────────────────────────────────────────────────────────
102
+ app.include_router(health.router, prefix="/api/v1", tags=["health"])
103
+ app.include_router(tasks.router, prefix="/api/v1/tasks", tags=["tasks"])
104
+ app.include_router(chat.router, prefix="/api/v1", tags=["chat"])
105
+ app.include_router(memory.router, prefix="/api/v1/memory", tags=["memory"])
106
+ app.include_router(github.router, prefix="/api/v1/github", tags=["github"])
107
+
108
+
109
+ # ─── WebSocket Endpoints ───────────────────────────────────────────────────────
110
+ @app.websocket("/ws/tasks/{task_id}")
111
+ async def ws_task(websocket: WebSocket, task_id: str):
112
+ """Live streaming for specific task execution."""
113
+ await ws_manager.connect(websocket, room=f"task:{task_id}")
114
+ try:
115
+ while True:
116
+ data = await websocket.receive_text()
117
+ msg = json.loads(data)
118
+ if msg.get("type") == "ping":
119
+ await websocket.send_json({"type": "pong", "timestamp": time.time()})
120
+ except WebSocketDisconnect:
121
+ ws_manager.disconnect(websocket, room=f"task:{task_id}")
122
+
123
+
124
+ @app.websocket("/ws/logs")
125
+ async def ws_logs(websocket: WebSocket):
126
+ """Global live log stream."""
127
+ await ws_manager.connect(websocket, room="logs")
128
+ try:
129
+ while True:
130
+ data = await websocket.receive_text()
131
+ msg = json.loads(data)
132
+ if msg.get("type") == "ping":
133
+ await websocket.send_json({"type": "pong", "timestamp": time.time()})
134
+ except WebSocketDisconnect:
135
+ ws_manager.disconnect(websocket, room="logs")
136
+
137
+
138
+ @app.websocket("/ws/chat/{session_id}")
139
+ async def ws_chat(websocket: WebSocket, session_id: str):
140
+ """Real-time chat streaming per session."""
141
+ await ws_manager.connect(websocket, room=f"chat:{session_id}")
142
+ try:
143
+ while True:
144
+ data = await websocket.receive_text()
145
+ msg = json.loads(data)
146
+ if msg.get("type") == "ping":
147
+ await websocket.send_json({"type": "pong", "timestamp": time.time()})
148
+ elif msg.get("type") == "chat_message":
149
+ # Trigger streaming chat response
150
+ asyncio.create_task(
151
+ task_engine.handle_chat_message(session_id, msg.get("content", ""), websocket)
152
+ )
153
+ except WebSocketDisconnect:
154
+ ws_manager.disconnect(websocket, room=f"chat:{session_id}")
155
+
156
+
157
+ @app.websocket("/ws/agent/status")
158
+ async def ws_agent_status(websocket: WebSocket):
159
+ """Global agent status stream."""
160
+ await ws_manager.connect(websocket, room="agent_status")
161
+ try:
162
+ while True:
163
+ data = await websocket.receive_text()
164
+ msg = json.loads(data)
165
+ if msg.get("type") == "ping":
166
+ await websocket.send_json({"type": "pong", "timestamp": time.time()})
167
+ except WebSocketDisconnect:
168
+ ws_manager.disconnect(websocket, room="agent_status")
169
+
170
+
171
+ # ─── Root ──────────────────────────────────────────────────────────────────────
172
+ @app.get("/")
173
+ async def root():
174
+ return {
175
+ "name": "🤖 Devin Agent Platform",
176
+ "version": "2.0.0",
177
+ "status": "operational",
178
+ "docs": "/api/docs",
179
+ "websockets": ["/ws/tasks/{task_id}", "/ws/logs", "/ws/chat/{session_id}", "/ws/agent/status"],
180
+ }
root/etc/s6-overlay/s6-rc.d/svc-openvscode-server/dependencies.d/init-services → memory/__init__.py RENAMED
File without changes
memory/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (146 Bytes). View file
 
memory/__pycache__/db.cpython-312.pyc ADDED
Binary file (19.5 kB). View file
 
memory/db.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Production SQLite Database — Async via aiosqlite
3
+ Handles tasks, memory, sessions, events
4
+ """
5
+
6
+ import aiosqlite
7
+ import os
8
+ import json
9
+ import time
10
+ from typing import Optional, List, Dict, Any
11
+ import structlog
12
+
13
+ log = structlog.get_logger()
14
+
15
+ DB_PATH = os.environ.get("DB_PATH", "/tmp/devin_agent.db")
16
+
17
+
18
+ async def get_db() -> aiosqlite.Connection:
19
+ db = await aiosqlite.connect(DB_PATH)
20
+ db.row_factory = aiosqlite.Row
21
+ await db.execute("PRAGMA journal_mode=WAL")
22
+ await db.execute("PRAGMA foreign_keys=ON")
23
+ return db
24
+
25
+
26
+ async def init_db():
27
+ """Initialize all tables."""
28
+ log.info("Initializing database", path=DB_PATH)
29
+ async with aiosqlite.connect(DB_PATH) as db:
30
+ await db.execute("PRAGMA journal_mode=WAL")
31
+ await db.execute("PRAGMA foreign_keys=ON")
32
+
33
+ # Tasks table
34
+ await db.execute("""
35
+ CREATE TABLE IF NOT EXISTS tasks (
36
+ id TEXT PRIMARY KEY,
37
+ session_id TEXT,
38
+ project_id TEXT,
39
+ goal TEXT NOT NULL,
40
+ status TEXT DEFAULT 'queued',
41
+ plan TEXT,
42
+ result TEXT,
43
+ error TEXT,
44
+ metadata TEXT DEFAULT '{}',
45
+ created_at REAL,
46
+ started_at REAL,
47
+ completed_at REAL,
48
+ retry_count INTEGER DEFAULT 0
49
+ )
50
+ """)
51
+
52
+ # Task events table
53
+ await db.execute("""
54
+ CREATE TABLE IF NOT EXISTS task_events (
55
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
56
+ task_id TEXT NOT NULL,
57
+ event_type TEXT NOT NULL,
58
+ data TEXT DEFAULT '{}',
59
+ timestamp REAL,
60
+ FOREIGN KEY (task_id) REFERENCES tasks(id)
61
+ )
62
+ """)
63
+
64
+ # Memory table
65
+ await db.execute("""
66
+ CREATE TABLE IF NOT EXISTS memory (
67
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
68
+ session_id TEXT,
69
+ project_id TEXT,
70
+ memory_type TEXT NOT NULL,
71
+ key TEXT,
72
+ content TEXT NOT NULL,
73
+ metadata TEXT DEFAULT '{}',
74
+ embedding TEXT,
75
+ created_at REAL,
76
+ updated_at REAL
77
+ )
78
+ """)
79
+
80
+ # Sessions table
81
+ await db.execute("""
82
+ CREATE TABLE IF NOT EXISTS sessions (
83
+ id TEXT PRIMARY KEY,
84
+ project_id TEXT,
85
+ user_id TEXT,
86
+ metadata TEXT DEFAULT '{}',
87
+ created_at REAL,
88
+ last_active REAL
89
+ )
90
+ """)
91
+
92
+ # GitHub operations table
93
+ await db.execute("""
94
+ CREATE TABLE IF NOT EXISTS github_ops (
95
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
96
+ task_id TEXT,
97
+ operation TEXT NOT NULL,
98
+ repo TEXT,
99
+ branch TEXT,
100
+ status TEXT DEFAULT 'pending',
101
+ result TEXT,
102
+ created_at REAL
103
+ )
104
+ """)
105
+
106
+ # Indexes
107
+ await db.execute("CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id)")
108
+ await db.execute("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)")
109
+ await db.execute("CREATE INDEX IF NOT EXISTS idx_events_task ON task_events(task_id)")
110
+ await db.execute("CREATE INDEX IF NOT EXISTS idx_memory_session ON memory(session_id)")
111
+ await db.execute("CREATE INDEX IF NOT EXISTS idx_memory_project ON memory(project_id)")
112
+ await db.execute("CREATE INDEX IF NOT EXISTS idx_memory_type ON memory(memory_type)")
113
+
114
+ await db.commit()
115
+ log.info("✅ Database initialized")
116
+
117
+
118
+ # ─── Task CRUD ─────────────────────────────────────────────────────────────────
119
+
120
+ async def create_task(task_id: str, goal: str, session_id: str = "", project_id: str = "", metadata: dict = {}):
121
+ async with aiosqlite.connect(DB_PATH) as db:
122
+ await db.execute("""
123
+ INSERT INTO tasks (id, session_id, project_id, goal, status, metadata, created_at)
124
+ VALUES (?, ?, ?, ?, 'queued', ?, ?)
125
+ """, (task_id, session_id, project_id, goal, json.dumps(metadata), time.time()))
126
+ await db.commit()
127
+
128
+
129
+ async def update_task_status(task_id: str, status: str, **kwargs):
130
+ fields = ["status = ?"]
131
+ values = [status]
132
+ if status == "executing":
133
+ fields.append("started_at = ?")
134
+ values.append(time.time())
135
+ if status in ("completed", "failed", "cancelled"):
136
+ fields.append("completed_at = ?")
137
+ values.append(time.time())
138
+ for k, v in kwargs.items():
139
+ if k in ("plan", "result", "error"):
140
+ fields.append(f"{k} = ?")
141
+ values.append(v if isinstance(v, str) else json.dumps(v))
142
+ elif k == "retry_count":
143
+ fields.append("retry_count = ?")
144
+ values.append(v)
145
+ values.append(task_id)
146
+ async with aiosqlite.connect(DB_PATH) as db:
147
+ await db.execute(f"UPDATE tasks SET {', '.join(fields)} WHERE id = ?", values)
148
+ await db.commit()
149
+
150
+
151
+ async def get_task(task_id: str) -> Optional[Dict]:
152
+ async with aiosqlite.connect(DB_PATH) as db:
153
+ db.row_factory = aiosqlite.Row
154
+ async with db.execute("SELECT * FROM tasks WHERE id = ?", (task_id,)) as cursor:
155
+ row = await cursor.fetchone()
156
+ if row:
157
+ d = dict(row)
158
+ d["metadata"] = json.loads(d.get("metadata") or "{}")
159
+ d["plan"] = json.loads(d["plan"]) if d.get("plan") else None
160
+ return d
161
+ return None
162
+
163
+
164
+ async def list_tasks(session_id: str = "", limit: int = 50) -> List[Dict]:
165
+ async with aiosqlite.connect(DB_PATH) as db:
166
+ db.row_factory = aiosqlite.Row
167
+ if session_id:
168
+ async with db.execute(
169
+ "SELECT * FROM tasks WHERE session_id = ? ORDER BY created_at DESC LIMIT ?",
170
+ (session_id, limit)
171
+ ) as cursor:
172
+ rows = await cursor.fetchall()
173
+ else:
174
+ async with db.execute(
175
+ "SELECT * FROM tasks ORDER BY created_at DESC LIMIT ?", (limit,)
176
+ ) as cursor:
177
+ rows = await cursor.fetchall()
178
+ return [dict(r) for r in rows]
179
+
180
+
181
+ async def save_task_event(task_id: str, event_type: str, data: dict = {}):
182
+ async with aiosqlite.connect(DB_PATH) as db:
183
+ await db.execute("""
184
+ INSERT INTO task_events (task_id, event_type, data, timestamp)
185
+ VALUES (?, ?, ?, ?)
186
+ """, (task_id, event_type, json.dumps(data), time.time()))
187
+ await db.commit()
188
+
189
+
190
+ async def get_task_events(task_id: str) -> List[Dict]:
191
+ async with aiosqlite.connect(DB_PATH) as db:
192
+ db.row_factory = aiosqlite.Row
193
+ async with db.execute(
194
+ "SELECT * FROM task_events WHERE task_id = ? ORDER BY timestamp ASC", (task_id,)
195
+ ) as cursor:
196
+ rows = await cursor.fetchall()
197
+ return [dict(r) for r in rows]
198
+
199
+
200
+ # ─── Memory CRUD ───────────────────────────────────────────────────────────────
201
+
202
+ async def save_memory(
203
+ content: str,
204
+ memory_type: str,
205
+ session_id: str = "",
206
+ project_id: str = "",
207
+ key: str = "",
208
+ metadata: dict = {}
209
+ ):
210
+ now = time.time()
211
+ async with aiosqlite.connect(DB_PATH) as db:
212
+ await db.execute("""
213
+ INSERT INTO memory (session_id, project_id, memory_type, key, content, metadata, created_at, updated_at)
214
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
215
+ """, (session_id, project_id, memory_type, key, content, json.dumps(metadata), now, now))
216
+ await db.commit()
217
+
218
+
219
+ async def search_memory(query: str, session_id: str = "", project_id: str = "", limit: int = 20) -> List[Dict]:
220
+ """Simple keyword search (upgrade to vector search in production)."""
221
+ async with aiosqlite.connect(DB_PATH) as db:
222
+ db.row_factory = aiosqlite.Row
223
+ q = f"%{query}%"
224
+ if session_id:
225
+ async with db.execute(
226
+ "SELECT * FROM memory WHERE session_id = ? AND content LIKE ? ORDER BY updated_at DESC LIMIT ?",
227
+ (session_id, q, limit)
228
+ ) as cursor:
229
+ rows = await cursor.fetchall()
230
+ elif project_id:
231
+ async with db.execute(
232
+ "SELECT * FROM memory WHERE project_id = ? AND content LIKE ? ORDER BY updated_at DESC LIMIT ?",
233
+ (project_id, q, limit)
234
+ ) as cursor:
235
+ rows = await cursor.fetchall()
236
+ else:
237
+ async with db.execute(
238
+ "SELECT * FROM memory WHERE content LIKE ? ORDER BY updated_at DESC LIMIT ?",
239
+ (q, limit)
240
+ ) as cursor:
241
+ rows = await cursor.fetchall()
242
+ return [dict(r) for r in rows]
243
+
244
+
245
+ async def get_project_memory(project_id: str, memory_type: str = "", limit: int = 100) -> List[Dict]:
246
+ async with aiosqlite.connect(DB_PATH) as db:
247
+ db.row_factory = aiosqlite.Row
248
+ if memory_type:
249
+ async with db.execute(
250
+ "SELECT * FROM memory WHERE project_id = ? AND memory_type = ? ORDER BY updated_at DESC LIMIT ?",
251
+ (project_id, memory_type, limit)
252
+ ) as cursor:
253
+ rows = await cursor.fetchall()
254
+ else:
255
+ async with db.execute(
256
+ "SELECT * FROM memory WHERE project_id = ? ORDER BY updated_at DESC LIMIT ?",
257
+ (project_id, limit)
258
+ ) as cursor:
259
+ rows = await cursor.fetchall()
260
+ return [dict(r) for r in rows]
261
+
262
+
263
+ async def get_history(session_id: str, limit: int = 50) -> List[Dict]:
264
+ async with aiosqlite.connect(DB_PATH) as db:
265
+ db.row_factory = aiosqlite.Row
266
+ async with db.execute(
267
+ "SELECT * FROM memory WHERE session_id = ? AND memory_type = 'conversation' ORDER BY created_at DESC LIMIT ?",
268
+ (session_id, limit)
269
+ ) as cursor:
270
+ rows = await cursor.fetchall()
271
+ return [dict(r) for r in rows]
nginx.conf DELETED
@@ -1,129 +0,0 @@
1
- error_log /tmp/error.log warn;
2
- worker_processes auto;
3
- pid /tmp/nginx.pid;
4
- include /etc/nginx/modules-enabled/*.conf;
5
-
6
- events {
7
- worker_connections 768;
8
- multi_accept on;
9
- }
10
-
11
- http {
12
- ##
13
- # Basic Settings
14
- ##
15
-
16
- sendfile on;
17
- tcp_nopush on;
18
- tcp_nodelay on;
19
- keepalive_timeout 65;
20
- types_hash_max_size 2048;
21
- proxy_buffering off;
22
- client_max_body_size 800m;
23
- large_client_header_buffers 4 32k;
24
- # server_tokens off;
25
-
26
- # server_names_hash_bucket_size 64;
27
- # server_name_in_redirect off;
28
-
29
- include /etc/nginx/mime.types;
30
-
31
- default_type application/octet-stream;
32
- proxy_temp_path /tmp/proxy_temp;
33
- client_body_temp_path /tmp/client_temp;
34
- fastcgi_temp_path /tmp/fastcgi_temp;
35
- uwsgi_temp_path /tmp/uwsgi_temp;
36
- scgi_temp_path /tmp/scgi_temp;
37
-
38
- ##
39
- # SSL Settings
40
- ##
41
-
42
- ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
43
- ssl_prefer_server_ciphers on;
44
-
45
-
46
- ##
47
- # Gzip Settings
48
- ##
49
-
50
- gzip on;
51
-
52
- # gzip_vary on;
53
- # gzip_proxied any;
54
- # gzip_comp_level 6;
55
- # gzip_buffers 16 8k;
56
- # gzip_http_version 1.1;
57
- # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
58
-
59
- ##
60
- # Virtual Host Configs
61
- ##
62
-
63
- #include /etc/nginx/conf.d/*.conf;
64
- #include /etc/nginx/sites-enabled/*;
65
- server {
66
- listen 7860;
67
-
68
- access_log /tmp/access.log;
69
- server_name _;
70
-
71
- root /var/www/;
72
- index index.html;
73
- location /stable-#COMMIT# {
74
- proxy_pass http://127.0.0.1:5050;
75
- proxy_http_version 1.1;
76
- proxy_set_header Upgrade $http_upgrade;
77
- proxy_set_header Connection "Upgrade";
78
- proxy_set_header Host $host;
79
- proxy_read_timeout 86400;
80
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
81
- }
82
-
83
- location /vscode/ {
84
- auth_basic "Restricted Content";
85
- auth_basic_user_file /home/user/app/ngpasswd;
86
- proxy_pass http://127.0.0.1:5050/;
87
- proxy_http_version 1.1;
88
- proxy_set_header Upgrade $http_upgrade;
89
- proxy_set_header Connection "Upgrade";
90
- proxy_set_header Host $host;
91
- proxy_read_timeout 86400;
92
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
93
- }
94
-
95
- location @vscode {
96
- return 302 https://$host/vscode/?folder=/home/user/app;
97
- }
98
-
99
- error_page 502 = @vscode;
100
- location /api/ {
101
- proxy_pass http://127.0.0.1:8000/api/;
102
- proxy_http_version 1.1;
103
- proxy_set_header Upgrade $http_upgrade;
104
- proxy_set_header Connection "Upgrade";
105
- proxy_set_header Host $host;
106
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
107
- }
108
-
109
- location /ws/ {
110
- proxy_pass http://127.0.0.1:8000/ws/;
111
- proxy_http_version 1.1;
112
- proxy_set_header Upgrade $http_upgrade;
113
- proxy_set_header Connection "Upgrade";
114
- proxy_set_header Host $host;
115
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
116
- }
117
-
118
- location / {
119
- proxy_pass http://127.0.0.1:#PORT#;
120
- proxy_http_version 1.1;
121
- proxy_set_header Upgrade $http_upgrade;
122
- proxy_set_header Connection "Upgrade";
123
- proxy_set_header Host $host;
124
- proxy_read_timeout 86400;
125
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
126
- #try_files $uri $uri/ =404;
127
- }
128
- }
129
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
on_startup.sh DELETED
@@ -1,31 +0,0 @@
1
- #!/bin/bash
2
- # Write some commands here that will run on root user before startup.
3
- # For example, to clone transformers and install it in dev mode:
4
- # git clone https://github.com/huggingface/transformers.git
5
- # cd transformers && pip install -e ".[dev]"
6
-
7
- npm i -g tsx tslab http-server miniflare@2
8
- sudo chown -R 1000:1000 "/home/user/"
9
- tslab install
10
-
11
- echo '
12
- # >>> conda initialize >>>
13
- __conda_setup="$(/home/user/miniconda/bin/conda shell.bash hook 2> /dev/null)"
14
- if [ $? -eq 0 ]; then
15
- eval "$__conda_setup"
16
- else
17
- if [ -f "/home/user/miniconda/etc/profile.d/conda.sh" ]; then
18
- . "/home/user/miniconda/etc/profile.d/conda.sh"
19
- else
20
- export PATH="/home/user/miniconda/bin:$PATH"
21
- fi
22
- fi
23
- unset __conda_setup
24
- # <<< conda initialize <<<
25
- ' >> ~/.bashrc
26
-
27
- apt-config dump | grep Sandbox::User
28
- cat <<EOF > /etc/apt/apt.conf.d/sandbox-disable
29
- APT::Sandbox::User "root";
30
- EOF
31
- sudo chown -R 1000:1000 "/usr/"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
packages.txt DELETED
@@ -1 +0,0 @@
1
- tree
 
 
requirements.txt CHANGED
@@ -1,12 +1,28 @@
1
- # jupyterlab
2
- jupyterlab==3.6.1
3
- jupyter-server==2.3.0
4
- fastapi
5
- uvicorn
6
- python-jose[cryptography]
7
- python-multipart
8
- pydantic
9
- websockets
10
- requests
11
- aiohttp[speedups]
12
- brotli
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.111.0
2
+ uvicorn[standard]==0.29.0
3
+ websockets==12.0
4
+ pydantic==2.7.1
5
+ pydantic-settings==2.2.1
6
+ python-jose[cryptography]==3.3.0
7
+ python-multipart==0.0.9
8
+ aiohttp==3.9.5
9
+ aiosqlite==0.20.0
10
+ sqlalchemy[asyncio]==2.0.30
11
+ alembic==1.13.1
12
+ httpx==0.27.0
13
+ openai==1.30.1
14
+ anthropic==0.26.1
15
+ gitpython==3.1.43
16
+ pygithub==2.3.0
17
+ python-dotenv==1.0.1
18
+ slowapi==0.1.9
19
+ structlog==24.1.0
20
+ rich==13.7.1
21
+ asyncio-mqtt==0.16.2
22
+ redis==5.0.4
23
+ celery==5.3.6
24
+ passlib[bcrypt]==1.7.4
25
+ cryptography==42.0.7
26
+ typer==0.12.3
27
+ watchfiles==0.21.0
28
+ psutil==5.9.8
root/etc/s6-overlay/s6-rc.d/init-openvscode-server/run DELETED
@@ -1,35 +0,0 @@
1
- #!/usr/bin/with-contenv bash
2
-
3
- mkdir -p /config/{workspace,.ssh}
4
-
5
- if [ -n "${SUDO_PASSWORD}" ] || [ -n "${SUDO_PASSWORD_HASH}" ]; then
6
- echo "setting up sudo access"
7
- if ! grep -q 'abc' /etc/sudoers; then
8
- echo "adding abc to sudoers"
9
- echo "abc ALL=(ALL:ALL) ALL" >> /etc/sudoers
10
- fi
11
- if [ -n "${SUDO_PASSWORD_HASH}" ]; then
12
- echo "setting sudo password using sudo password hash"
13
- sed -i "s|^abc:\!:|abc:${SUDO_PASSWORD_HASH}:|" /etc/shadow
14
- else
15
- echo "setting sudo password using SUDO_PASSWORD env var"
16
- echo -e "${SUDO_PASSWORD}\n${SUDO_PASSWORD}" | passwd abc
17
- fi
18
- fi
19
-
20
- [[ ! -f /config/.bashrc ]] && \
21
- cp /root/.bashrc /config/.bashrc
22
- [[ ! -f /config/.profile ]] && \
23
- cp /root/.profile /config/.profile
24
-
25
- # fix permissions (ignore contents of /config/workspace)
26
- echo "setting permissions::config"
27
- find /config -path /config/workspace -prune -o -exec chown abc:abc {} +
28
- chown abc:abc /config/workspace
29
- echo "setting permissions::app"
30
- chown -R abc:abc /app/openvscode-server
31
-
32
- chmod 700 /config/.ssh
33
- if [ -n "$(ls -A /config/.ssh)" ]; then
34
- chmod 600 /config/.ssh/*
35
- fi
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
root/etc/s6-overlay/s6-rc.d/init-openvscode-server/type DELETED
@@ -1 +0,0 @@
1
- oneshot
 
 
root/etc/s6-overlay/s6-rc.d/init-openvscode-server/up DELETED
@@ -1 +0,0 @@
1
- /etc/s6-overlay/s6-rc.d/init-openvscode-server/run
 
 
root/etc/s6-overlay/s6-rc.d/svc-openvscode-server/notification-fd DELETED
@@ -1 +0,0 @@
1
- 3