maur 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- maur-0.1.0/PKG-INFO +210 -0
- maur-0.1.0/README.md +196 -0
- maur-0.1.0/pyproject.toml +30 -0
- maur-0.1.0/src/maur/__init__.py +0 -0
- maur-0.1.0/src/maur/api.py +141 -0
- maur-0.1.0/src/maur/app.py +40 -0
- maur-0.1.0/src/maur/components.py +39 -0
- maur-0.1.0/src/maur/git.py +145 -0
- maur-0.1.0/src/maur/linear.py +43 -0
- maur-0.1.0/src/maur/models.py +111 -0
- maur-0.1.0/src/maur/opencode.py +125 -0
- maur-0.1.0/src/maur/proc.py +53 -0
- maur-0.1.0/src/maur/schemas.py +26 -0
- maur-0.1.0/src/maur/settings.py +65 -0
- maur-0.1.0/src/maur/worker.py +176 -0
maur-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: maur
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Mats E. Mollestad
|
|
6
|
+
Author-email: Mats E. Mollestad <mats@mollestad.no>
|
|
7
|
+
Requires-Dist: fastapi
|
|
8
|
+
Requires-Dist: sqlalchemy[asyncio]
|
|
9
|
+
Requires-Dist: sqlmodel
|
|
10
|
+
Requires-Dist: takk>=0.1.25
|
|
11
|
+
Requires-Dist: asyncpg
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Maur 🐜
|
|
16
|
+
|
|
17
|
+
Inspired by [Strip's Minions](https://stripe.dev/blog/minions-stripes-one-shot-end-to-end-coding-agents). Maur (Norwegian for "ants") is an autonomous coding agent that integrates into your Python projects. It receives tasks from various sources — production error alerts, Slack, Linear issues, or direct API calls — clones your repo, runs an AI coding agent ([OpenCode](https://opencode.ai)), and opens a pull/merge request with the fix.
|
|
18
|
+
|
|
19
|
+
## How it works
|
|
20
|
+
|
|
21
|
+
1. A task arrives via webhook or direct API call
|
|
22
|
+
2. The API stores the task and publishes it to a message queue
|
|
23
|
+
3. A worker picks up the task, clones your repo into a temporary workspace
|
|
24
|
+
4. OpenCode runs against the cloned repo using your configured LLM
|
|
25
|
+
5. If changes are made, they are committed and pushed to a new branch (`maur/<task-id>`)
|
|
26
|
+
6. A pull request (GitHub) or merge request (GitLab) is opened automatically
|
|
27
|
+
|
|
28
|
+
## Architecture
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
[Trigger source] [maur_api] [maur_code_subscriber]
|
|
32
|
+
Linear webhook ---> FastAPI app ---> Worker (OpenCode)
|
|
33
|
+
Exception alert stores task clones repo
|
|
34
|
+
Manual POST /tasks publishes msg runs agent
|
|
35
|
+
opens PR/MR
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The two components are deployed separately via `takk`:
|
|
39
|
+
- **`maur_api`** — lightweight FastAPI service that authenticates requests, persists tasks, and enqueues work
|
|
40
|
+
- **`maur_code_subscriber`** — NATS subscriber that processes tasks one at a time using OpenCode
|
|
41
|
+
|
|
42
|
+
## Prerequisites
|
|
43
|
+
|
|
44
|
+
- Python ≥ 3.10
|
|
45
|
+
- [`takk`](https://pypi.org/project/takk/) for infrastructure management
|
|
46
|
+
- A NATS server (provisioned by `takk`)
|
|
47
|
+
- A PostgreSQL or MySQL database (provisioned by `takk`)
|
|
48
|
+
- An OpenAI-compatible LLM API (e.g. [OpenRouter](https://openrouter.ai), a local Ollama instance, or any provider with an OpenAI-compatible endpoint. `takk` default to using Ollama unless you overwrite the env vars.)
|
|
49
|
+
- A GitHub or GitLab repository with a token that has push and PR/MR creation permissions
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv add maur
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Basic usage
|
|
58
|
+
|
|
59
|
+
### 1. Add the infrastructure
|
|
60
|
+
|
|
61
|
+
Add both components to your `project.py` file:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from maur.components import maur_api, maur_code_subscriber
|
|
65
|
+
|
|
66
|
+
project = Project(
|
|
67
|
+
name="your-project",
|
|
68
|
+
|
|
69
|
+
# The API that authenticates and enqueues tasks
|
|
70
|
+
maur_api=maur_api(),
|
|
71
|
+
|
|
72
|
+
# The worker that clones the repo, runs OpenCode, and opens a PR/MR
|
|
73
|
+
# Pass secrets for any environment variables your target repo needs at build time
|
|
74
|
+
maur_coder=maur_code_subscriber(secrets=[...]),
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 2. Configure secrets
|
|
79
|
+
|
|
80
|
+
Run `takk dotenv` to regenerate your `.env` file, then fill in the required values:
|
|
81
|
+
|
|
82
|
+
| Variable | Required | Description |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `DB_URI` | No | PostgreSQL (`postgresql+asyncpg://...`) or MySQL (`mysql+aiomysql://...`) connection URI |
|
|
85
|
+
| `NATS_URI` | No | NATS (`nats://...`) |
|
|
86
|
+
| `MAUR_BEARER_TOKEN` | Yes | Secret token used to authenticate API requests |
|
|
87
|
+
| `MAUR_LLM_TOKEN` | No | API key for your LLM provider |
|
|
88
|
+
| `MAUR_LLM_API` | No | Base URL of your OpenAI-compatible LLM API |
|
|
89
|
+
| `MAUR_LLM_MODEL` | No | Model to use (default: `qwen3.5:4b`) |
|
|
90
|
+
| `GITHUB_REPO_URL` | Yes* | HTTPS URL of the GitHub repo to clone and open PRs on |
|
|
91
|
+
| `GITHUB_TOKEN` | Yes* | GitHub personal access token with `repo` scope |
|
|
92
|
+
| `GITHUB_API_URL` | No | GitHub API base URL (default: `https://api.github.com`) |
|
|
93
|
+
| `GITLAB_REPO_URL` | Yes* | HTTPS URL of the GitLab repo |
|
|
94
|
+
| `GITLAB_TOKEN` | Yes* | GitLab personal access token with `api` scope |
|
|
95
|
+
| `GITLAB_URL` | No | GitLab instance URL (default: `https://gitlab.com`) |
|
|
96
|
+
|
|
97
|
+
\* Provide either the GitHub **or** GitLab set of variables. GitLab takes precedence if both are set.
|
|
98
|
+
|
|
99
|
+
### 3. Start the system
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
takk up
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Both the API and worker containers will be built and started.
|
|
106
|
+
|
|
107
|
+
## API reference
|
|
108
|
+
|
|
109
|
+
All endpoints (except `/health`) require a `Bearer` token in the `Authorization` header matching `MAUR_BEARER_TOKEN`.
|
|
110
|
+
|
|
111
|
+
### POST `/tasks` — Manual task
|
|
112
|
+
|
|
113
|
+
Send any arbitrary prompt to the agent.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
curl -X POST http://localhost:8000/tasks \
|
|
117
|
+
-H "Authorization: Bearer <token>" \
|
|
118
|
+
-H "Content-Type: application/json" \
|
|
119
|
+
-d '{
|
|
120
|
+
"prompt": "Refactor the payment module to use the new Stripe SDK",
|
|
121
|
+
"source_id": "unique-identifier-for-dedup",
|
|
122
|
+
"repo_branch": "main"
|
|
123
|
+
}'
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### POST `/webhooks/exception` — Exception alert
|
|
127
|
+
|
|
128
|
+
Send a production error for the agent to fix. `fingerprint` is used for deduplication — tasks with the same fingerprint that are already pending or in progress are rejected.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
curl -X POST http://localhost:8000/webhooks/exception \
|
|
132
|
+
-H "Authorization: Bearer <token>" \
|
|
133
|
+
-H "Content-Type: application/json" \
|
|
134
|
+
-d '{
|
|
135
|
+
"fingerprint": "KeyError-user-profile-views-42",
|
|
136
|
+
"title": "KeyError: '\''email'\'' in user_profile view",
|
|
137
|
+
"description": "Traceback (most recent call last):\n ...",
|
|
138
|
+
"repo_branch": "main",
|
|
139
|
+
"extra": {"environment": "production", "user_id": 123}
|
|
140
|
+
}'
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### POST `/webhooks/linear` — Linear webhook
|
|
144
|
+
|
|
145
|
+
Configure a [Linear webhook](https://linear.app/docs/webhooks) to send issue events here. Maur extracts the issue title, description, and repository URL (must be attached to the issue) and opens a PR with the fix.
|
|
146
|
+
|
|
147
|
+
Set the webhook URL to: `https://<your-api-host>/webhooks/linear`
|
|
148
|
+
|
|
149
|
+
Optionally set `LINEAR_WEBHOOK_SECRET` in your environment to verify webhook signatures.
|
|
150
|
+
|
|
151
|
+
### GET `/tasks` — List tasks
|
|
152
|
+
|
|
153
|
+
Returns the 50 most recent tasks.
|
|
154
|
+
|
|
155
|
+
### GET `/tasks/{task_id}` — Get task
|
|
156
|
+
|
|
157
|
+
Returns the status and result of a specific task.
|
|
158
|
+
|
|
159
|
+
### GET `/health`
|
|
160
|
+
|
|
161
|
+
Returns `"ok"`. Used for health checks.
|
|
162
|
+
|
|
163
|
+
## Customisation
|
|
164
|
+
|
|
165
|
+
### Changing the LLM model
|
|
166
|
+
|
|
167
|
+
Set `MAUR_LLM_MODEL` to any model available through your `MAUR_LLM_API` provider. The worker uses OpenCode with an OpenAI-compatible provider, so any model exposed via that protocol works.
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
MAUR_LLM_MODEL=devstral-2-123b-instruct-2512
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Adjusting worker compute resources
|
|
174
|
+
|
|
175
|
+
The default worker is allocated 3 GB of memory. Override this via the `compute` argument:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from takk.models import Compute
|
|
179
|
+
from maur.components import maur_code_subscriber
|
|
180
|
+
|
|
181
|
+
maur_coder=maur_code_subscriber(
|
|
182
|
+
compute=Compute(mb_memory_limit=1024 * 8) # 8 GB
|
|
183
|
+
)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Passing additional secrets to the worker
|
|
187
|
+
|
|
188
|
+
If your target repository requires environment variables at build or runtime (e.g. private package indexes), pass them through `secrets`:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from maur.components import maur_code_subscriber
|
|
192
|
+
from my_project.settings import MyPrivateRegistrySettings
|
|
193
|
+
|
|
194
|
+
maur_coder=maur_code_subscriber(
|
|
195
|
+
secrets=[MyPrivateRegistrySettings, ...]
|
|
196
|
+
)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Development
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# Install dependencies
|
|
203
|
+
uv sync --all-groups
|
|
204
|
+
|
|
205
|
+
# Lint
|
|
206
|
+
uv run ruff check .
|
|
207
|
+
|
|
208
|
+
# Type check
|
|
209
|
+
uv run ty check
|
|
210
|
+
```
|
maur-0.1.0/README.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Maur 🐜
|
|
2
|
+
|
|
3
|
+
Inspired by [Strip's Minions](https://stripe.dev/blog/minions-stripes-one-shot-end-to-end-coding-agents). Maur (Norwegian for "ants") is an autonomous coding agent that integrates into your Python projects. It receives tasks from various sources — production error alerts, Slack, Linear issues, or direct API calls — clones your repo, runs an AI coding agent ([OpenCode](https://opencode.ai)), and opens a pull/merge request with the fix.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
1. A task arrives via webhook or direct API call
|
|
8
|
+
2. The API stores the task and publishes it to a message queue
|
|
9
|
+
3. A worker picks up the task, clones your repo into a temporary workspace
|
|
10
|
+
4. OpenCode runs against the cloned repo using your configured LLM
|
|
11
|
+
5. If changes are made, they are committed and pushed to a new branch (`maur/<task-id>`)
|
|
12
|
+
6. A pull request (GitHub) or merge request (GitLab) is opened automatically
|
|
13
|
+
|
|
14
|
+
## Architecture
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
[Trigger source] [maur_api] [maur_code_subscriber]
|
|
18
|
+
Linear webhook ---> FastAPI app ---> Worker (OpenCode)
|
|
19
|
+
Exception alert stores task clones repo
|
|
20
|
+
Manual POST /tasks publishes msg runs agent
|
|
21
|
+
opens PR/MR
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The two components are deployed separately via `takk`:
|
|
25
|
+
- **`maur_api`** — lightweight FastAPI service that authenticates requests, persists tasks, and enqueues work
|
|
26
|
+
- **`maur_code_subscriber`** — NATS subscriber that processes tasks one at a time using OpenCode
|
|
27
|
+
|
|
28
|
+
## Prerequisites
|
|
29
|
+
|
|
30
|
+
- Python ≥ 3.10
|
|
31
|
+
- [`takk`](https://pypi.org/project/takk/) for infrastructure management
|
|
32
|
+
- A NATS server (provisioned by `takk`)
|
|
33
|
+
- A PostgreSQL or MySQL database (provisioned by `takk`)
|
|
34
|
+
- An OpenAI-compatible LLM API (e.g. [OpenRouter](https://openrouter.ai), a local Ollama instance, or any provider with an OpenAI-compatible endpoint. `takk` default to using Ollama unless you overwrite the env vars.)
|
|
35
|
+
- A GitHub or GitLab repository with a token that has push and PR/MR creation permissions
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv add maur
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Basic usage
|
|
44
|
+
|
|
45
|
+
### 1. Add the infrastructure
|
|
46
|
+
|
|
47
|
+
Add both components to your `project.py` file:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from maur.components import maur_api, maur_code_subscriber
|
|
51
|
+
|
|
52
|
+
project = Project(
|
|
53
|
+
name="your-project",
|
|
54
|
+
|
|
55
|
+
# The API that authenticates and enqueues tasks
|
|
56
|
+
maur_api=maur_api(),
|
|
57
|
+
|
|
58
|
+
# The worker that clones the repo, runs OpenCode, and opens a PR/MR
|
|
59
|
+
# Pass secrets for any environment variables your target repo needs at build time
|
|
60
|
+
maur_coder=maur_code_subscriber(secrets=[...]),
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2. Configure secrets
|
|
65
|
+
|
|
66
|
+
Run `takk dotenv` to regenerate your `.env` file, then fill in the required values:
|
|
67
|
+
|
|
68
|
+
| Variable | Required | Description |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `DB_URI` | No | PostgreSQL (`postgresql+asyncpg://...`) or MySQL (`mysql+aiomysql://...`) connection URI |
|
|
71
|
+
| `NATS_URI` | No | NATS (`nats://...`) |
|
|
72
|
+
| `MAUR_BEARER_TOKEN` | Yes | Secret token used to authenticate API requests |
|
|
73
|
+
| `MAUR_LLM_TOKEN` | No | API key for your LLM provider |
|
|
74
|
+
| `MAUR_LLM_API` | No | Base URL of your OpenAI-compatible LLM API |
|
|
75
|
+
| `MAUR_LLM_MODEL` | No | Model to use (default: `qwen3.5:4b`) |
|
|
76
|
+
| `GITHUB_REPO_URL` | Yes* | HTTPS URL of the GitHub repo to clone and open PRs on |
|
|
77
|
+
| `GITHUB_TOKEN` | Yes* | GitHub personal access token with `repo` scope |
|
|
78
|
+
| `GITHUB_API_URL` | No | GitHub API base URL (default: `https://api.github.com`) |
|
|
79
|
+
| `GITLAB_REPO_URL` | Yes* | HTTPS URL of the GitLab repo |
|
|
80
|
+
| `GITLAB_TOKEN` | Yes* | GitLab personal access token with `api` scope |
|
|
81
|
+
| `GITLAB_URL` | No | GitLab instance URL (default: `https://gitlab.com`) |
|
|
82
|
+
|
|
83
|
+
\* Provide either the GitHub **or** GitLab set of variables. GitLab takes precedence if both are set.
|
|
84
|
+
|
|
85
|
+
### 3. Start the system
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
takk up
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Both the API and worker containers will be built and started.
|
|
92
|
+
|
|
93
|
+
## API reference
|
|
94
|
+
|
|
95
|
+
All endpoints (except `/health`) require a `Bearer` token in the `Authorization` header matching `MAUR_BEARER_TOKEN`.
|
|
96
|
+
|
|
97
|
+
### POST `/tasks` — Manual task
|
|
98
|
+
|
|
99
|
+
Send any arbitrary prompt to the agent.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
curl -X POST http://localhost:8000/tasks \
|
|
103
|
+
-H "Authorization: Bearer <token>" \
|
|
104
|
+
-H "Content-Type: application/json" \
|
|
105
|
+
-d '{
|
|
106
|
+
"prompt": "Refactor the payment module to use the new Stripe SDK",
|
|
107
|
+
"source_id": "unique-identifier-for-dedup",
|
|
108
|
+
"repo_branch": "main"
|
|
109
|
+
}'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### POST `/webhooks/exception` — Exception alert
|
|
113
|
+
|
|
114
|
+
Send a production error for the agent to fix. `fingerprint` is used for deduplication — tasks with the same fingerprint that are already pending or in progress are rejected.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
curl -X POST http://localhost:8000/webhooks/exception \
|
|
118
|
+
-H "Authorization: Bearer <token>" \
|
|
119
|
+
-H "Content-Type: application/json" \
|
|
120
|
+
-d '{
|
|
121
|
+
"fingerprint": "KeyError-user-profile-views-42",
|
|
122
|
+
"title": "KeyError: '\''email'\'' in user_profile view",
|
|
123
|
+
"description": "Traceback (most recent call last):\n ...",
|
|
124
|
+
"repo_branch": "main",
|
|
125
|
+
"extra": {"environment": "production", "user_id": 123}
|
|
126
|
+
}'
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### POST `/webhooks/linear` — Linear webhook
|
|
130
|
+
|
|
131
|
+
Configure a [Linear webhook](https://linear.app/docs/webhooks) to send issue events here. Maur extracts the issue title, description, and repository URL (must be attached to the issue) and opens a PR with the fix.
|
|
132
|
+
|
|
133
|
+
Set the webhook URL to: `https://<your-api-host>/webhooks/linear`
|
|
134
|
+
|
|
135
|
+
Optionally set `LINEAR_WEBHOOK_SECRET` in your environment to verify webhook signatures.
|
|
136
|
+
|
|
137
|
+
### GET `/tasks` — List tasks
|
|
138
|
+
|
|
139
|
+
Returns the 50 most recent tasks.
|
|
140
|
+
|
|
141
|
+
### GET `/tasks/{task_id}` — Get task
|
|
142
|
+
|
|
143
|
+
Returns the status and result of a specific task.
|
|
144
|
+
|
|
145
|
+
### GET `/health`
|
|
146
|
+
|
|
147
|
+
Returns `"ok"`. Used for health checks.
|
|
148
|
+
|
|
149
|
+
## Customisation
|
|
150
|
+
|
|
151
|
+
### Changing the LLM model
|
|
152
|
+
|
|
153
|
+
Set `MAUR_LLM_MODEL` to any model available through your `MAUR_LLM_API` provider. The worker uses OpenCode with an OpenAI-compatible provider, so any model exposed via that protocol works.
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
MAUR_LLM_MODEL=devstral-2-123b-instruct-2512
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Adjusting worker compute resources
|
|
160
|
+
|
|
161
|
+
The default worker is allocated 3 GB of memory. Override this via the `compute` argument:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from takk.models import Compute
|
|
165
|
+
from maur.components import maur_code_subscriber
|
|
166
|
+
|
|
167
|
+
maur_coder=maur_code_subscriber(
|
|
168
|
+
compute=Compute(mb_memory_limit=1024 * 8) # 8 GB
|
|
169
|
+
)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Passing additional secrets to the worker
|
|
173
|
+
|
|
174
|
+
If your target repository requires environment variables at build or runtime (e.g. private package indexes), pass them through `secrets`:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from maur.components import maur_code_subscriber
|
|
178
|
+
from my_project.settings import MyPrivateRegistrySettings
|
|
179
|
+
|
|
180
|
+
maur_coder=maur_code_subscriber(
|
|
181
|
+
secrets=[MyPrivateRegistrySettings, ...]
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Development
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# Install dependencies
|
|
189
|
+
uv sync --all-groups
|
|
190
|
+
|
|
191
|
+
# Lint
|
|
192
|
+
uv run ruff check .
|
|
193
|
+
|
|
194
|
+
# Type check
|
|
195
|
+
uv run ty check
|
|
196
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "maur"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Mats E. Mollestad", email = "mats@mollestad.no" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"fastapi",
|
|
12
|
+
"sqlalchemy[asyncio]",
|
|
13
|
+
"sqlmodel",
|
|
14
|
+
"takk>=0.1.25",
|
|
15
|
+
"asyncpg"
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
maur = "maur:main"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["uv_build>=0.9.9,<0.10.0"]
|
|
23
|
+
build-backend = "uv_build"
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = [
|
|
27
|
+
"pytest>=9.0.2",
|
|
28
|
+
"ruff>=0.15.5",
|
|
29
|
+
"ty>=0.0.21",
|
|
30
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
from fastapi.security import OAuth2PasswordBearer
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Depends, Header, HTTPException, Request
|
|
7
|
+
from sqlmodel import select
|
|
8
|
+
|
|
9
|
+
from maur.models import CodingTask, SessionDep
|
|
10
|
+
from maur.settings import MaurSettings
|
|
11
|
+
from maur.schemas import (
|
|
12
|
+
ExceptionPayload,
|
|
13
|
+
LinearWebhookPayload,
|
|
14
|
+
ManualTaskPayload,
|
|
15
|
+
TaskResponse,
|
|
16
|
+
)
|
|
17
|
+
from maur import linear as linear_client
|
|
18
|
+
from maur.components import coding_tasks_pubsub, CodingTaskMessage
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
router = APIRouter()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
bearer = OAuth2PasswordBearer(tokenUrl="/api/v1/users/token")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@lru_cache
|
|
30
|
+
def maur_settings() -> MaurSettings:
|
|
31
|
+
return MaurSettings() # type: ignore
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
SettingsDep = Annotated[MaurSettings, Depends(maur_settings)]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def raise_on_unauthenticated(
|
|
38
|
+
token: Annotated[str, Depends(bearer)]
|
|
39
|
+
) -> None:
|
|
40
|
+
settings = maur_settings()
|
|
41
|
+
|
|
42
|
+
if token != settings.maur_bearer_token.get_secret_value():
|
|
43
|
+
raise HTTPException(status_code=403)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def _dedup_check(session: SessionDep, source_id: str) -> CodingTask | None:
|
|
47
|
+
result = await session.exec(
|
|
48
|
+
select(CodingTask)
|
|
49
|
+
.where(CodingTask.source_id == source_id)
|
|
50
|
+
.where(CodingTask.status.in_(["pending", "in_progress"])) # type: ignore[union-attr]
|
|
51
|
+
)
|
|
52
|
+
return result.first()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@router.post(
|
|
56
|
+
"/webhooks/exception",
|
|
57
|
+
dependencies=[Depends(raise_on_unauthenticated)]
|
|
58
|
+
)
|
|
59
|
+
async def exception_webhook(
|
|
60
|
+
payload: ExceptionPayload,
|
|
61
|
+
session: SessionDep,
|
|
62
|
+
) -> TaskResponse:
|
|
63
|
+
|
|
64
|
+
existing = await _dedup_check(session, payload.fingerprint)
|
|
65
|
+
if existing:
|
|
66
|
+
raise HTTPException(
|
|
67
|
+
status_code=409,
|
|
68
|
+
detail={"message": "Task already in progress", "task_id": str(existing.id)},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
prompt = (
|
|
72
|
+
f"Fix the following exception:\n\nTitle: {payload.title}\n\n"
|
|
73
|
+
f"Stack trace / details:\n{payload.description}"
|
|
74
|
+
)
|
|
75
|
+
if payload.extra:
|
|
76
|
+
prompt += f"\n\nExtra context:\n{payload.extra}"
|
|
77
|
+
|
|
78
|
+
task = CodingTask(
|
|
79
|
+
source="exception",
|
|
80
|
+
source_id=payload.fingerprint,
|
|
81
|
+
repo_branch=payload.repo_branch,
|
|
82
|
+
prompt=prompt,
|
|
83
|
+
)
|
|
84
|
+
await task.insert(session)
|
|
85
|
+
await coding_tasks_pubsub.publish(CodingTaskMessage(task_id=task.id))
|
|
86
|
+
|
|
87
|
+
return TaskResponse(task_id=task.id, status="pending", message="Task queued")
|
|
88
|
+
|
|
89
|
+
@router.post(
|
|
90
|
+
"/tasks",
|
|
91
|
+
dependencies=[Depends(raise_on_unauthenticated)]
|
|
92
|
+
)
|
|
93
|
+
async def create_manual_task(
|
|
94
|
+
payload: ManualTaskPayload,
|
|
95
|
+
session: SessionDep,
|
|
96
|
+
) -> TaskResponse:
|
|
97
|
+
|
|
98
|
+
existing = await _dedup_check(session, payload.source_id)
|
|
99
|
+
if existing:
|
|
100
|
+
raise HTTPException(
|
|
101
|
+
status_code=409,
|
|
102
|
+
detail={"message": "Task already in progress", "task_id": str(existing.id)},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
task = CodingTask(
|
|
106
|
+
source="manual",
|
|
107
|
+
source_id=payload.source_id,
|
|
108
|
+
repo_branch=payload.repo_branch,
|
|
109
|
+
prompt=payload.prompt,
|
|
110
|
+
)
|
|
111
|
+
await task.insert(session)
|
|
112
|
+
await coding_tasks_pubsub.publish(CodingTaskMessage(task_id=task.id))
|
|
113
|
+
|
|
114
|
+
return TaskResponse(task_id=task.id, status="pending", message="Task queued")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@router.get(
|
|
118
|
+
"/tasks",
|
|
119
|
+
dependencies=[Depends(raise_on_unauthenticated)]
|
|
120
|
+
)
|
|
121
|
+
async def list_tasks(
|
|
122
|
+
session: SessionDep,
|
|
123
|
+
) -> list[CodingTask]:
|
|
124
|
+
result = await session.exec(select(CodingTask).order_by(CodingTask.created_at.desc()).limit(50)) # type: ignore[union-attr]
|
|
125
|
+
return list(result.all())
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@router.get(
|
|
129
|
+
"/tasks/{task_id}",
|
|
130
|
+
dependencies=[Depends(raise_on_unauthenticated)],
|
|
131
|
+
)
|
|
132
|
+
async def get_task(
|
|
133
|
+
task_id: str,
|
|
134
|
+
session: SessionDep,
|
|
135
|
+
) -> CodingTask:
|
|
136
|
+
import uuid as uuid_mod
|
|
137
|
+
|
|
138
|
+
task = await CodingTask.get(uuid_mod.UUID(task_id), session)
|
|
139
|
+
if task is None:
|
|
140
|
+
raise HTTPException(status_code=404, detail="Task not found")
|
|
141
|
+
return task
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
from sqlmodel import SQLModel
|
|
7
|
+
|
|
8
|
+
from maur.api import router
|
|
9
|
+
from maur.models import engine
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@asynccontextmanager
|
|
15
|
+
async def lifespan(app: FastAPI):
|
|
16
|
+
import logging
|
|
17
|
+
logging.basicConfig(level=logging.INFO)
|
|
18
|
+
|
|
19
|
+
logger.info("Creating db models")
|
|
20
|
+
async with engine().begin() as conn:
|
|
21
|
+
await conn.run_sync(SQLModel.metadata.create_all)
|
|
22
|
+
logger.info("Created all models")
|
|
23
|
+
|
|
24
|
+
yield
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
app = FastAPI(
|
|
28
|
+
title="Maur Agent",
|
|
29
|
+
description="Autonomous code agent that processes coding tasks from exception, Slack, and Linear.",
|
|
30
|
+
version="0.1.0",
|
|
31
|
+
lifespan=lifespan,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.get("/health")
|
|
36
|
+
def health() -> str:
|
|
37
|
+
return "ok"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
app.include_router(router)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from takk.models import SecretClass, Subscriber
|
|
2
|
+
from takk.secrets import NatsConfig
|
|
3
|
+
from maur.worker import CodingTaskMessage, process_coding_task
|
|
4
|
+
from maur.settings import GithubSettings, GitlabSettings, MaurSettings, db_setting_type, MaurLLMSettings
|
|
5
|
+
from takk import FastAPIApp, Compute, PubSub, DockerBuild
|
|
6
|
+
from takk.docker import ImageBuildArgs
|
|
7
|
+
|
|
8
|
+
coding_tasks_pubsub = PubSub(
|
|
9
|
+
content_type=CodingTaskMessage,
|
|
10
|
+
subject="maur.tasks",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
def maur_api(secrets: list[SecretClass] | None = None) -> FastAPIApp:
|
|
14
|
+
return FastAPIApp(
|
|
15
|
+
"maur.app",
|
|
16
|
+
health_check="/health",
|
|
17
|
+
secrets=secrets or [db_setting_type()],
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def maur_code_subscriber(
|
|
21
|
+
secrets: list[SecretClass] | None = None,
|
|
22
|
+
compute: Compute | None = None
|
|
23
|
+
) -> Subscriber[CodingTaskMessage]:
|
|
24
|
+
return coding_tasks_pubsub.subscriber(
|
|
25
|
+
process_coding_task,
|
|
26
|
+
image=DockerBuild.default_uv_image(
|
|
27
|
+
image_name="coder",
|
|
28
|
+
build_args={
|
|
29
|
+
ImageBuildArgs.uv_sync_args: "--locked --no-editable --active --all-groups",
|
|
30
|
+
ImageBuildArgs.apt_packages: "curl ca-certificates bash git libstdc++6 libgcc-s1 unzip jq grep",
|
|
31
|
+
ImageBuildArgs.install_opencode: "true"
|
|
32
|
+
}
|
|
33
|
+
),
|
|
34
|
+
secrets=secrets or [db_setting_type(), MaurSettings, MaurLLMSettings, GithubSettings, GitlabSettings, NatsConfig],
|
|
35
|
+
compute=compute or Compute(
|
|
36
|
+
mb_memory_limit=1024 * 3
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
|