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 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
+