-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtask_engine.py
More file actions
122 lines (105 loc) · 5.1 KB
/
Copy pathtask_engine.py
File metadata and controls
122 lines (105 loc) · 5.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
"""
task_engine.py – thin wrapper around the LangGraph pipeline (Phase 2).
All flow control now lives in graph/builder.py:
setup → load_skills → plan → code → detect_stack ⇄ phpunit / vitest /
↑ playwright layers
└── retry loop via on_test_failure and
on_frontend_test_failure
↓ all layers pass ↓ retries exhausted
commit failure
└──→ cleanup ←──────┘
detect_stack selects the applicable layers, so a PHP-only change never starts
a Node container and a frontend-only change never runs PHPUnit.
TaskEngine only:
1. Polls Redmine for the next pending issue (via graph.tools – no direct
RedmineClient / GitLabClient / DockerRunner imports here).
2. Seeds the initial IssueState and invokes the compiled graph.
3. Acts as the crash net: if the graph itself raises, the issue is
reopened and a Telegram alert is sent.
main.py is unchanged: it still constructs TaskEngine() and calls run_once().
"""
import logging
import os
from graph.builder import graph
from graph.state import IssueState
from graph.tools import tool_redmine_list_pending, tool_redmine_set_status
from telegram_notifier import notify as telegram_notify
logger = logging.getLogger(__name__)
class TaskEngine:
def __init__(self) -> None:
# All clients live behind graph/tools.py now; nothing to construct.
pass
def run_once(self) -> bool:
"""
Pick up one issue from Redmine and process it end-to-end via the graph.
Returns True if a Merge Request was successfully opened, False otherwise.
"""
# ── 1. Fetch next pending issue ───────────────────────────────────
listing = tool_redmine_list_pending()
if not listing["success"]:
logger.error("Failed to query Redmine for pending issues: %s", listing["error"])
return False
issues = listing["result"]
if not issues:
logger.info("No work to do. Exiting.")
return False
issue = issues[0] # oldest first (sort id:asc)
issue_id: int = issue["id"]
subject: str = issue.get("subject", f"issue-{issue_id}")
logger.info("Fetched issue #%s: %s", issue_id, subject)
max_attempts = int(os.environ.get("MAX_CODE_RETRIES", "2")) + 1
# ── 2. Seed initial state ─────────────────────────────────────────
initial_state: IssueState = {
"issue": issue,
"issue_id": issue_id,
"subject": subject,
"skills": [],
"plan": "",
"code_response": "",
"repo_path": "",
"branch_name": "",
"workspace": "",
"messages": [],
"attempt": 0,
"max_attempts": max_attempts,
"files_written": False,
"has_vue_files": False,
"stack": "php",
"run_phpunit": True,
"test_output": "",
"test_passed": False,
"vitest_passed": False,
"vitest_output": "",
"playwright_passed": False,
"playwright_output": "",
"mr_url": "",
"failure_reason": "",
"error": "",
}
# ── 3. Run the graph ──────────────────────────────────────────────
try:
# A fullstack retry cycle traverses 6 nodes (escalation → code →
# detect_stack → phpunit_test → vitest_test → playwright_test);
# size the recursion limit so large MAX_CODE_RETRIES values never
# trip LangGraph's default of 25.
final_state = graph.invoke(
initial_state,
config={"recursion_limit": max(25, 14 + 8 * max_attempts)},
)
if final_state.get("mr_url"):
logger.info("Issue #%s done – MR: %s", issue_id, final_state["mr_url"])
return True
logger.info(
"Issue #%s finished without MR: %s",
issue_id,
final_state.get("failure_reason") or final_state.get("error") or "unknown",
)
return False
except Exception as exc: # pylint: disable=broad-except
logger.exception("Graph execution failed for issue #%s: %s", issue_id, exc)
telegram_notify(
f"💥 Graph crashed on issue <b>#{issue_id}</b> – <i>{subject}</i>\n"
f"<code>{type(exc).__name__}: {exc}</code>"
)
tool_redmine_set_status(issue_id, "new", note=f"AI Developer crashed: {exc}")
return False