pyaesonegtckglay-dotcom commited on
Commit ·
33302b5
1
Parent(s): 36408aa
🚀 Upgrade to Devin Agent Platform v2.0
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +0 -34
- Dockerfile +34 -127
- README.md +52 -18
- __pycache__/main.cpython-312.pyc +0 -0
- ngpasswd → api/__init__.py +0 -0
- api/__pycache__/__init__.cpython-312.pyc +0 -0
- api/__pycache__/websocket_manager.cpython-312.pyc +0 -0
- root/etc/s6-overlay/s6-rc.d/init-config-end/dependencies.d/init-openvscode-server → api/routes/__init__.py +0 -0
- api/routes/__pycache__/__init__.cpython-312.pyc +0 -0
- api/routes/__pycache__/chat.cpython-312.pyc +0 -0
- api/routes/__pycache__/github.cpython-312.pyc +0 -0
- api/routes/__pycache__/health.cpython-312.pyc +0 -0
- api/routes/__pycache__/memory.cpython-312.pyc +0 -0
- api/routes/__pycache__/tasks.cpython-312.pyc +0 -0
- api/routes/chat.py +214 -0
- api/routes/github.py +336 -0
- api/routes/health.py +53 -0
- api/routes/memory.py +50 -0
- api/routes/tasks.py +167 -0
- api/websocket_manager.py +134 -0
- api_server.py +0 -44
- auth.py +0 -41
- core/__init__.py +0 -1
- core/__pycache__/__init__.cpython-312.pyc +0 -0
- core/__pycache__/agent.cpython-312.pyc +0 -0
- core/__pycache__/models.cpython-312.pyc +0 -0
- core/__pycache__/task_engine.cpython-312.pyc +0 -0
- core/agent.py +392 -0
- core/database.py +0 -132
- core/github_engine.py +0 -80
- core/llm_router.py +0 -37
- core/memory.py +0 -44
- core/models.py +213 -0
- core/orchestrator.py +0 -96
- core/task_engine.py +241 -0
- ecosystem.config.cjs +20 -0
- root/etc/s6-overlay/s6-rc.d/init-openvscode-server/dependencies.d/init-config → github/__init__.py +0 -0
- main.py +180 -0
- root/etc/s6-overlay/s6-rc.d/svc-openvscode-server/dependencies.d/init-services → memory/__init__.py +0 -0
- memory/__pycache__/__init__.cpython-312.pyc +0 -0
- memory/__pycache__/db.cpython-312.pyc +0 -0
- memory/db.py +271 -0
- nginx.conf +0 -129
- on_startup.sh +0 -31
- packages.txt +0 -1
- requirements.txt +28 -12
- root/etc/s6-overlay/s6-rc.d/init-openvscode-server/run +0 -35
- root/etc/s6-overlay/s6-rc.d/init-openvscode-server/type +0 -1
- root/etc/s6-overlay/s6-rc.d/init-openvscode-server/up +0 -1
- 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
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 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 |
-
#
|
| 77 |
-
RUN
|
| 78 |
-
|
| 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 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
# End root user section
|
| 116 |
-
#######################################
|
| 117 |
|
| 118 |
-
|
|
|
|
| 119 |
|
| 120 |
-
#
|
| 121 |
-
RUN -
|
| 122 |
-
pip install --no-cache-dir --upgrade -r requirements.txt
|
| 123 |
|
| 124 |
-
#
|
| 125 |
-
|
|
|
|
| 126 |
|
| 127 |
-
|
| 128 |
|
| 129 |
-
|
| 130 |
|
| 131 |
-
ENV
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
SYSTEM=spaces \
|
| 137 |
-
SHELL=/bin/bash
|
| 138 |
|
| 139 |
-
|
|
|
|
|
|
|
| 140 |
|
| 141 |
-
CMD ["
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
| 14 |
-
# Autonomous Coding System (Upgraded)
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 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 |
-
##
|
| 22 |
-
|
| 23 |
-
-
|
| 24 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
python-jose[cryptography]
|
| 7 |
-
python-multipart
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|