loop-mcp 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.
- loop_mcp-0.1.0/.gitignore +56 -0
- loop_mcp-0.1.0/PKG-INFO +103 -0
- loop_mcp-0.1.0/README.md +72 -0
- loop_mcp-0.1.0/pyproject.toml +64 -0
- loop_mcp-0.1.0/src/loop_engineering_mcp/__init__.py +7 -0
- loop_mcp-0.1.0/src/loop_engineering_mcp/__main__.py +37 -0
- loop_mcp-0.1.0/src/loop_engineering_mcp/github_client.py +130 -0
- loop_mcp-0.1.0/src/loop_engineering_mcp/logger.py +60 -0
- loop_mcp-0.1.0/src/loop_engineering_mcp/loop_executor.py +297 -0
- loop_mcp-0.1.0/src/loop_engineering_mcp/loop_manager.py +191 -0
- loop_mcp-0.1.0/src/loop_engineering_mcp/retry.py +36 -0
- loop_mcp-0.1.0/src/loop_engineering_mcp/scheduler.py +105 -0
- loop_mcp-0.1.0/src/loop_engineering_mcp/server.py +378 -0
- loop_mcp-0.1.0/src/loop_engineering_mcp/skill_manager.py +117 -0
- loop_mcp-0.1.0/src/loop_engineering_mcp/state_manager.py +339 -0
- loop_mcp-0.1.0/src/loop_engineering_mcp/verification_runner.py +97 -0
- loop_mcp-0.1.0/src/loop_engineering_mcp/worker.py +53 -0
- loop_mcp-0.1.0/tests/test_logger.py +30 -0
- loop_mcp-0.1.0/tests/test_retry.py +42 -0
- loop_mcp-0.1.0/tests/test_scheduler.py +49 -0
- loop_mcp-0.1.0/tests/test_state_manager.py +53 -0
- loop_mcp-0.1.0/tests/test_verification_runner.py +46 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Secrets (never commit)
|
|
2
|
+
.pypirc
|
|
3
|
+
.npmrc
|
|
4
|
+
|
|
5
|
+
# Python
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
*$py.class
|
|
9
|
+
*.so
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
venv/
|
|
27
|
+
env/
|
|
28
|
+
ENV/
|
|
29
|
+
|
|
30
|
+
# Node
|
|
31
|
+
node_modules/
|
|
32
|
+
npm-debug.log*
|
|
33
|
+
yarn-debug.log*
|
|
34
|
+
yarn-error.log*
|
|
35
|
+
dist/
|
|
36
|
+
build/
|
|
37
|
+
*.tsbuildinfo
|
|
38
|
+
|
|
39
|
+
# IDE
|
|
40
|
+
.vscode/
|
|
41
|
+
.idea/
|
|
42
|
+
*.swp
|
|
43
|
+
*.swo
|
|
44
|
+
*~
|
|
45
|
+
.DS_Store
|
|
46
|
+
|
|
47
|
+
# Loop state (local only)
|
|
48
|
+
.loop/
|
|
49
|
+
|
|
50
|
+
# Testing
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
coverage/
|
|
53
|
+
.nyc_output/
|
|
54
|
+
*.log
|
|
55
|
+
|
|
56
|
+
\IMPLEMENTATION_COMPLETE.md
|
loop_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: loop-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for automated loop engineering in AI coding workflows
|
|
5
|
+
Project-URL: Homepage, https://github.com/yourusername/loop-engineering
|
|
6
|
+
Project-URL: Documentation, https://github.com/yourusername/loop-engineering#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/yourusername/loop-engineering
|
|
8
|
+
Project-URL: Issues, https://github.com/yourusername/loop-engineering/issues
|
|
9
|
+
Author: Loop Engineering Contributors
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: ai,automation,coding,loop-engineering,mcp
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: aiofiles>=23.0.0
|
|
21
|
+
Requires-Dist: croniter>=2.0.0
|
|
22
|
+
Requires-Dist: httpx>=0.27.0
|
|
23
|
+
Requires-Dist: mcp>=0.9.0
|
|
24
|
+
Requires-Dist: pydantic>=2.0.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# Loop Engineering MCP Server (Python)
|
|
33
|
+
|
|
34
|
+
Python implementation of the Loop Engineering MCP server.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
### Using uvx (recommended - no installation needed)
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uvx loop-mcp
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Using pip
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install loop-mcp
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
Add to your MCP configuration file:
|
|
53
|
+
|
|
54
|
+
**Cursor** (`.cursor/mcp.json`):
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"loop-engineering": {
|
|
59
|
+
"command": "uvx",
|
|
60
|
+
"args": ["loop-mcp"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Kiro** (`.kiro/settings/mcp.json`):
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"mcpServers": {
|
|
70
|
+
"loop-engineering": {
|
|
71
|
+
"command": "uvx",
|
|
72
|
+
"args": ["loop-mcp"]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Claude Desktop**:
|
|
79
|
+
- Mac: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
80
|
+
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"mcpServers": {
|
|
85
|
+
"loop-engineering": {
|
|
86
|
+
"command": "uvx",
|
|
87
|
+
"args": ["loop-mcp"]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
cd python
|
|
97
|
+
pip install -e ".[dev]"
|
|
98
|
+
pytest
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
loop_mcp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Loop Engineering MCP Server (Python)
|
|
2
|
+
|
|
3
|
+
Python implementation of the Loop Engineering MCP server.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### Using uvx (recommended - no installation needed)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uvx loop-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Using pip
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install loop-mcp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Configuration
|
|
20
|
+
|
|
21
|
+
Add to your MCP configuration file:
|
|
22
|
+
|
|
23
|
+
**Cursor** (`.cursor/mcp.json`):
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"loop-engineering": {
|
|
28
|
+
"command": "uvx",
|
|
29
|
+
"args": ["loop-mcp"]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Kiro** (`.kiro/settings/mcp.json`):
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"loop-engineering": {
|
|
40
|
+
"command": "uvx",
|
|
41
|
+
"args": ["loop-mcp"]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Claude Desktop**:
|
|
48
|
+
- Mac: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
49
|
+
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"loop-engineering": {
|
|
55
|
+
"command": "uvx",
|
|
56
|
+
"args": ["loop-mcp"]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
cd python
|
|
66
|
+
pip install -e ".[dev]"
|
|
67
|
+
pytest
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "loop-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server for automated loop engineering in AI coding workflows"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
keywords = ["mcp", "ai", "coding", "automation", "loop-engineering"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Loop Engineering Contributors" }
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"mcp>=0.9.0",
|
|
27
|
+
"pydantic>=2.0.0",
|
|
28
|
+
"aiofiles>=23.0.0",
|
|
29
|
+
"httpx>=0.27.0",
|
|
30
|
+
"croniter>=2.0.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=7.0.0",
|
|
36
|
+
"pytest-asyncio>=0.21.0",
|
|
37
|
+
"black>=23.0.0",
|
|
38
|
+
"ruff>=0.1.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/yourusername/loop-engineering"
|
|
43
|
+
Documentation = "https://github.com/yourusername/loop-engineering#readme"
|
|
44
|
+
Repository = "https://github.com/yourusername/loop-engineering"
|
|
45
|
+
Issues = "https://github.com/yourusername/loop-engineering/issues"
|
|
46
|
+
|
|
47
|
+
[project.scripts]
|
|
48
|
+
loop-mcp = "loop_engineering_mcp.__main__:main"
|
|
49
|
+
loop-mcp-worker = "loop_engineering_mcp.worker:main"
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
asyncio_mode = "auto"
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.wheel]
|
|
56
|
+
packages = ["src/loop_engineering_mcp"]
|
|
57
|
+
|
|
58
|
+
[tool.black]
|
|
59
|
+
line-length = 100
|
|
60
|
+
target-version = ["py310"]
|
|
61
|
+
|
|
62
|
+
[tool.ruff]
|
|
63
|
+
line-length = 100
|
|
64
|
+
target-version = "py310"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Entry point for the Loop Engineering MCP server."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import sys
|
|
5
|
+
from mcp.server.stdio import stdio_server
|
|
6
|
+
from .server import create_server
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def _run_server():
|
|
10
|
+
"""Run the MCP server with stdio transport."""
|
|
11
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
12
|
+
server = create_server(start_scheduler=True)
|
|
13
|
+
scheduler = getattr(server, "_loop_scheduler", None)
|
|
14
|
+
if scheduler:
|
|
15
|
+
scheduler.start()
|
|
16
|
+
try:
|
|
17
|
+
init_options = server.create_initialization_options()
|
|
18
|
+
await server.run(read_stream, write_stream, init_options)
|
|
19
|
+
finally:
|
|
20
|
+
if scheduler:
|
|
21
|
+
await scheduler.stop()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main():
|
|
25
|
+
"""Run the MCP server."""
|
|
26
|
+
try:
|
|
27
|
+
asyncio.run(_run_server())
|
|
28
|
+
except KeyboardInterrupt:
|
|
29
|
+
print("\nShutting down Loop Engineering MCP server...", file=sys.stderr)
|
|
30
|
+
sys.exit(0)
|
|
31
|
+
except Exception as e:
|
|
32
|
+
print(f"Error running server: {e}", file=sys.stderr)
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
main()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""GitHub API integration for PR creation."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .retry import retry_async
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class PullRequestResult:
|
|
15
|
+
"""Result of creating a pull request."""
|
|
16
|
+
|
|
17
|
+
success: bool
|
|
18
|
+
pr_number: Optional[int] = None
|
|
19
|
+
pr_url: Optional[str] = None
|
|
20
|
+
branch: Optional[str] = None
|
|
21
|
+
error: Optional[str] = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GitHubClient:
|
|
25
|
+
"""Creates branches and pull requests via the GitHub REST API."""
|
|
26
|
+
|
|
27
|
+
API_BASE = "https://api.github.com"
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
token: Optional[str] = None,
|
|
32
|
+
repo: Optional[str] = None,
|
|
33
|
+
default_branch: str = "main",
|
|
34
|
+
):
|
|
35
|
+
self.token = token or os.environ.get("GITHUB_TOKEN")
|
|
36
|
+
self.repo = repo or os.environ.get("GITHUB_REPO") or self._detect_repo()
|
|
37
|
+
self.default_branch = default_branch or os.environ.get("GITHUB_DEFAULT_BRANCH", "main")
|
|
38
|
+
|
|
39
|
+
def _detect_repo(self) -> Optional[str]:
|
|
40
|
+
try:
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
["git", "remote", "get-url", "origin"],
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
timeout=10,
|
|
46
|
+
)
|
|
47
|
+
if result.returncode != 0:
|
|
48
|
+
return None
|
|
49
|
+
url = result.stdout.strip()
|
|
50
|
+
# Handle git@github.com:owner/repo.git and https://github.com/owner/repo.git
|
|
51
|
+
if "github.com" in url:
|
|
52
|
+
parts = url.rstrip(".git").split("/")
|
|
53
|
+
if len(parts) >= 2:
|
|
54
|
+
return f"{parts[-2]}/{parts[-1]}"
|
|
55
|
+
return None
|
|
56
|
+
except Exception:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
def _headers(self) -> dict[str, str]:
|
|
60
|
+
if not self.token:
|
|
61
|
+
raise ValueError("GITHUB_TOKEN environment variable is required")
|
|
62
|
+
return {
|
|
63
|
+
"Authorization": f"Bearer {self.token}",
|
|
64
|
+
"Accept": "application/vnd.github+json",
|
|
65
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async def create_pull_request(
|
|
69
|
+
self,
|
|
70
|
+
*,
|
|
71
|
+
title: str,
|
|
72
|
+
body: str,
|
|
73
|
+
branch: str,
|
|
74
|
+
base: Optional[str] = None,
|
|
75
|
+
) -> PullRequestResult:
|
|
76
|
+
"""Create a pull request on GitHub."""
|
|
77
|
+
if not self.repo:
|
|
78
|
+
return PullRequestResult(
|
|
79
|
+
success=False,
|
|
80
|
+
error="Could not detect GitHub repo. Set GITHUB_REPO=owner/repo",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
base_branch = base or self.default_branch
|
|
84
|
+
|
|
85
|
+
async def _create() -> PullRequestResult:
|
|
86
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
87
|
+
response = await client.post(
|
|
88
|
+
f"{self.API_BASE}/repos/{self.repo}/pulls",
|
|
89
|
+
headers=self._headers(),
|
|
90
|
+
json={
|
|
91
|
+
"title": title,
|
|
92
|
+
"body": body,
|
|
93
|
+
"head": branch,
|
|
94
|
+
"base": base_branch,
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
if response.status_code == 422:
|
|
98
|
+
data = response.json()
|
|
99
|
+
errors = data.get("errors", [])
|
|
100
|
+
msg = errors[0].get("message") if errors else data.get("message", "Validation failed")
|
|
101
|
+
return PullRequestResult(success=False, error=msg)
|
|
102
|
+
response.raise_for_status()
|
|
103
|
+
data = response.json()
|
|
104
|
+
return PullRequestResult(
|
|
105
|
+
success=True,
|
|
106
|
+
pr_number=data["number"],
|
|
107
|
+
pr_url=data["html_url"],
|
|
108
|
+
branch=branch,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
return await retry_async(_create, max_attempts=3)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
return PullRequestResult(success=False, error=str(e))
|
|
115
|
+
|
|
116
|
+
async def push_branch(self, branch: str, cwd: Optional[str] = None) -> tuple[bool, str]:
|
|
117
|
+
"""Push the current branch to origin."""
|
|
118
|
+
try:
|
|
119
|
+
result = subprocess.run(
|
|
120
|
+
["git", "push", "-u", "origin", branch],
|
|
121
|
+
capture_output=True,
|
|
122
|
+
text=True,
|
|
123
|
+
timeout=120,
|
|
124
|
+
cwd=cwd,
|
|
125
|
+
)
|
|
126
|
+
if result.returncode != 0:
|
|
127
|
+
return False, result.stderr or result.stdout
|
|
128
|
+
return True, "Branch pushed successfully"
|
|
129
|
+
except Exception as e:
|
|
130
|
+
return False, str(e)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Structured logging for loop engineering."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LoopLogger:
|
|
11
|
+
"""Writes structured JSON logs to .loop/logs/."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, logs_dir: Path):
|
|
14
|
+
self.logs_dir = logs_dir
|
|
15
|
+
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
self._logger = logging.getLogger("loop_engineering")
|
|
17
|
+
if not self._logger.handlers:
|
|
18
|
+
handler = logging.StreamHandler()
|
|
19
|
+
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
|
|
20
|
+
self._logger.addHandler(handler)
|
|
21
|
+
self._logger.setLevel(logging.INFO)
|
|
22
|
+
|
|
23
|
+
def _write_json(self, filename: str, entry: dict[str, Any]) -> None:
|
|
24
|
+
log_file = self.logs_dir / filename
|
|
25
|
+
with open(log_file, "a", encoding="utf-8") as f:
|
|
26
|
+
f.write(json.dumps(entry, default=str) + "\n")
|
|
27
|
+
|
|
28
|
+
def _entry(
|
|
29
|
+
self,
|
|
30
|
+
level: str,
|
|
31
|
+
event: str,
|
|
32
|
+
loop_name: Optional[str] = None,
|
|
33
|
+
**extra: Any,
|
|
34
|
+
) -> dict[str, Any]:
|
|
35
|
+
return {
|
|
36
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
37
|
+
"level": level,
|
|
38
|
+
"event": event,
|
|
39
|
+
"loop_name": loop_name,
|
|
40
|
+
**extra,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def info(self, event: str, loop_name: Optional[str] = None, **extra: Any) -> None:
|
|
44
|
+
entry = self._entry("INFO", event, loop_name, **extra)
|
|
45
|
+
self._write_json("loop-engineering.log", entry)
|
|
46
|
+
self._logger.info(f"[{loop_name or 'system'}] {event}")
|
|
47
|
+
|
|
48
|
+
def warning(self, event: str, loop_name: Optional[str] = None, **extra: Any) -> None:
|
|
49
|
+
entry = self._entry("WARNING", event, loop_name, **extra)
|
|
50
|
+
self._write_json("loop-engineering.log", entry)
|
|
51
|
+
self._logger.warning(f"[{loop_name or 'system'}] {event}")
|
|
52
|
+
|
|
53
|
+
def error(self, event: str, loop_name: Optional[str] = None, **extra: Any) -> None:
|
|
54
|
+
entry = self._entry("ERROR", event, loop_name, **extra)
|
|
55
|
+
self._write_json("loop-engineering.log", entry)
|
|
56
|
+
self._logger.error(f"[{loop_name or 'system'}] {event}")
|
|
57
|
+
|
|
58
|
+
def run_log(self, loop_name: str, run_id: str, data: dict[str, Any]) -> None:
|
|
59
|
+
entry = self._entry("INFO", "loop_run", loop_name, run_id=run_id, **data)
|
|
60
|
+
self._write_json(f"{loop_name}-runs.log", entry)
|