polycoding 0.1.0__py3-none-any.whl
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.
- cli/__init__.py +53 -0
- cli/db.py +67 -0
- cli/flow.py +187 -0
- cli/main.py +44 -0
- cli/project.py +166 -0
- cli/server.py +127 -0
- cli/utils.py +70 -0
- cli/worker.py +124 -0
- github_app/__init__.py +13 -0
- github_app/app.py +224 -0
- github_app/auth.py +137 -0
- github_app/config.py +38 -0
- github_app/installation_manager.py +194 -0
- github_app/label_mapper.py +112 -0
- github_app/models.py +112 -0
- github_app/webhook_handler.py +217 -0
- persistence/__init__.py +5 -0
- persistence/config.py +12 -0
- persistence/postgres.py +346 -0
- persistence/registry.py +111 -0
- persistence/tasks.py +178 -0
- polycoding-0.1.0.dist-info/METADATA +225 -0
- polycoding-0.1.0.dist-info/RECORD +41 -0
- polycoding-0.1.0.dist-info/WHEEL +4 -0
- polycoding-0.1.0.dist-info/entry_points.txt +3 -0
- polycoding-0.1.0.dist-info/licenses/LICENSE +20 -0
- project_manager/README.md +668 -0
- project_manager/__init__.py +29 -0
- project_manager/base.py +202 -0
- project_manager/config.py +36 -0
- project_manager/conversation/__init__.py +19 -0
- project_manager/conversation/flow.py +233 -0
- project_manager/conversation/types.py +64 -0
- project_manager/flow_runner.py +160 -0
- project_manager/git_utils.py +30 -0
- project_manager/github.py +367 -0
- project_manager/github_conversation.py +144 -0
- project_manager/github_projects_client.py +329 -0
- project_manager/hooks.py +377 -0
- project_manager/module.py +66 -0
- project_manager/types.py +79 -0
project_manager/base.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Abstract base class for project managers."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
from .types import Issue, ProjectConfig, ProjectItem
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProjectManager(ABC):
|
|
9
|
+
"""Abstract base class for project management providers."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, config: ProjectConfig) -> None:
|
|
12
|
+
"""Initialize project manager with configuration.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
config: Project configuration
|
|
16
|
+
"""
|
|
17
|
+
self.config = config
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def has_project(self) -> bool:
|
|
22
|
+
"""Is a project configured that can be used for management, e.g.
|
|
23
|
+
Kanban?
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def get_comments(self, issue_number: int) -> list:
|
|
28
|
+
"""Get all comments for an issue.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
issue_number: Issue number
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
List of comments
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def get_last_comment_by_user(self, issue_number: int, username: str) -> int | None:
|
|
39
|
+
"""Get the last comment ID by a specific user.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
issue_number: Issue number
|
|
43
|
+
username: GitHub username
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Comment ID if found, None otherwise
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def update_comment(self, issue_number: int, comment_id: int, body: str) -> bool:
|
|
51
|
+
"""Update an existing comment.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
issue_number: Issue number
|
|
55
|
+
comment_id: Comment ID to update
|
|
56
|
+
body: New comment body
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
True if successful, False otherwise
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def get_open_issues(self) -> list[Issue]:
|
|
64
|
+
"""Get all open issues from the repository.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
List of open issues
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def get_project_items(self) -> list[ProjectItem]:
|
|
72
|
+
"""Get all items in the project.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of project items
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def add_issue_to_project(self, issue: Issue) -> str | None:
|
|
80
|
+
"""Add an issue to the project.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
issue: Issue to add
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Project item ID if successful, None otherwise
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def update_issue_status(self, issue_number: int, status: str) -> bool:
|
|
91
|
+
"""Update the status of an issue in the project.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
issue_number: Issue number
|
|
95
|
+
status: New status value
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
True if successful, False otherwise
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def add_comment(self, issue_number: int, comment: str) -> bool:
|
|
103
|
+
"""Add a comment to an issue.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
issue_number: Issue number
|
|
107
|
+
comment: Comment text
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
True if successful, False otherwise
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
@abstractmethod
|
|
114
|
+
def has_label(self, issue_number: int, label_name: str) -> bool:
|
|
115
|
+
"""Check if an issue/PR has a specific label.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
issue_number: Issue or PR number
|
|
119
|
+
label_name: Name of the label to check for
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
True if label is present
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
@abstractmethod
|
|
126
|
+
def create_pull_request(
|
|
127
|
+
self,
|
|
128
|
+
title: str,
|
|
129
|
+
body: str,
|
|
130
|
+
head: str,
|
|
131
|
+
base: str = "develop",
|
|
132
|
+
) -> tuple[int, str] | None:
|
|
133
|
+
"""Create a pull request.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
title: PR title
|
|
137
|
+
body: PR body/description
|
|
138
|
+
head: Source branch name
|
|
139
|
+
base: Target branch name (default: "develop")
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Tuple of (pr_number, pr_url) if successful, None otherwise
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
@abstractmethod
|
|
146
|
+
def merge_pull_request(
|
|
147
|
+
self,
|
|
148
|
+
pr_number: int,
|
|
149
|
+
commit_message: str | None = None,
|
|
150
|
+
merge_method: str = "merge",
|
|
151
|
+
) -> bool:
|
|
152
|
+
"""Merge a pull request.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
pr_number: Pull request number
|
|
156
|
+
commit_message: Optional custom commit message for the merge
|
|
157
|
+
merge_method: Merge method - "merge", "squash", or "rebase"
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
True if successful
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
@abstractmethod
|
|
165
|
+
def bot_username(self) -> str:
|
|
166
|
+
"""Get the authenticated bot username."""
|
|
167
|
+
...
|
|
168
|
+
|
|
169
|
+
def find_project_item(self, issue_number: int) -> ProjectItem | None:
|
|
170
|
+
"""Find a project item by issue number.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
issue_number: Issue number
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Project item if found, None otherwise
|
|
177
|
+
"""
|
|
178
|
+
items = self.get_project_items()
|
|
179
|
+
for item in items:
|
|
180
|
+
if item.issue_number == issue_number:
|
|
181
|
+
return item
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
def sync_issues_to_project(self) -> int:
|
|
185
|
+
"""Sync all open issues to the project.
|
|
186
|
+
|
|
187
|
+
Adds any issues that aren't already in the project.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Number of issues added
|
|
191
|
+
"""
|
|
192
|
+
issues = self.get_open_issues()
|
|
193
|
+
items = self.get_project_items()
|
|
194
|
+
existing_numbers = {item.issue_number for item in items}
|
|
195
|
+
|
|
196
|
+
added = 0
|
|
197
|
+
for issue in issues:
|
|
198
|
+
if issue.number not in existing_numbers:
|
|
199
|
+
if self.add_issue_to_project(issue):
|
|
200
|
+
added += 1
|
|
201
|
+
|
|
202
|
+
return added
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ProjectManagerSettings(BaseSettings):
|
|
7
|
+
DATA_PATH: str = "/data"
|
|
8
|
+
|
|
9
|
+
GITHUB_APP_ID: Optional[str] = None
|
|
10
|
+
GITHUB_TOKEN: str
|
|
11
|
+
|
|
12
|
+
# Redis Configuration
|
|
13
|
+
REDIS_URL: str = "redis://localhost:6379/0"
|
|
14
|
+
REDIS_HOST: str = "localhost"
|
|
15
|
+
REDIS_PORT: int = 6379
|
|
16
|
+
REDIS_DB: int = 0
|
|
17
|
+
|
|
18
|
+
PROJECT_PROVIDER: Optional[str] = None
|
|
19
|
+
REPO_OWNER: Optional[str] = None
|
|
20
|
+
REPO_NAME: Optional[str] = None
|
|
21
|
+
PROJECT_IDENTIFIER: Optional[str] = None
|
|
22
|
+
|
|
23
|
+
# Label that must be present before merging a PR
|
|
24
|
+
MERGE_REQUIRED_LABEL: str = "polycode:automerge"
|
|
25
|
+
WORK_FLOW_START_LABEL: str = "polycode:implement"
|
|
26
|
+
|
|
27
|
+
# Label prefix for flow-triggering labels
|
|
28
|
+
FLOW_LABEL_PREFIX: str = "polycode:"
|
|
29
|
+
|
|
30
|
+
# Database Configuration
|
|
31
|
+
DATABASE_URL: str = "sqlite:///polycode.db"
|
|
32
|
+
|
|
33
|
+
model_config = SettingsConfigDict(extra="ignore", env_file=".env", case_sensitive=True)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
settings = ProjectManagerSettings() # pyright:ignore # ty:ignore
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Conversation-driven specification flow module."""
|
|
2
|
+
|
|
3
|
+
from .flow import ConversationFlow
|
|
4
|
+
from .types import (
|
|
5
|
+
ConversationFlowState,
|
|
6
|
+
ConversationMessage,
|
|
7
|
+
ConversationStage,
|
|
8
|
+
NewCommentInput,
|
|
9
|
+
ReactionInput,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ConversationFlow",
|
|
14
|
+
"ConversationFlowState",
|
|
15
|
+
"ConversationStage",
|
|
16
|
+
"ConversationMessage",
|
|
17
|
+
"NewCommentInput",
|
|
18
|
+
"ReactionInput",
|
|
19
|
+
]
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Conversation-driven specification flow."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
from crewai.flow.flow import listen, start
|
|
7
|
+
|
|
8
|
+
from crews.conversation_crew import ConversationCrew
|
|
9
|
+
from crews.plan_crew.types import Story
|
|
10
|
+
from flows.base import FlowIssueManagement
|
|
11
|
+
from project_manager.github_conversation import GitHubConversationManager
|
|
12
|
+
|
|
13
|
+
from .types import (
|
|
14
|
+
ConversationFlowState,
|
|
15
|
+
ConversationMessage,
|
|
16
|
+
ConversationStage,
|
|
17
|
+
NewCommentInput,
|
|
18
|
+
ReactionInput,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConversationFlow(FlowIssueManagement[ConversationFlowState]):
|
|
25
|
+
"""Flow for conversation-driven specification via GitHub comments."""
|
|
26
|
+
|
|
27
|
+
conversation_crew: ConversationCrew | None = None
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def _github_manager(self) -> GitHubConversationManager:
|
|
31
|
+
if not self.state.project_config:
|
|
32
|
+
raise ValueError("project_config not specified!")
|
|
33
|
+
return GitHubConversationManager(self.state.project_config)
|
|
34
|
+
|
|
35
|
+
@start()
|
|
36
|
+
def initialize_conversation(self):
|
|
37
|
+
"""Initialize conversation from issue opening."""
|
|
38
|
+
logger.info(f"🗣️ Starting conversation for issue #{self.state.issue_id}")
|
|
39
|
+
|
|
40
|
+
self.conversation_crew = ConversationCrew()
|
|
41
|
+
|
|
42
|
+
initial_message = (
|
|
43
|
+
"## 🤖 Specification Bot Activated\n\n"
|
|
44
|
+
"I'll help you refine this feature into a clear specification. "
|
|
45
|
+
"I'll ask questions to clarify requirements, then propose a specification for your approval.\n\n"
|
|
46
|
+
"**To approve a specification or story plan, add a 👍 reaction to my comment.**\n\n"
|
|
47
|
+
"Let's start!"
|
|
48
|
+
)
|
|
49
|
+
self._github_manager.add_comment(self.state.issue_id, initial_message)
|
|
50
|
+
self._ask_specification_question()
|
|
51
|
+
|
|
52
|
+
def _ask_specification_question(self):
|
|
53
|
+
"""Generate and post a specification question."""
|
|
54
|
+
crew = self.conversation_crew
|
|
55
|
+
if not crew:
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
history = "\n\n".join(f"**{msg.author}**: {msg.content}" for msg in self.state.messages)
|
|
59
|
+
|
|
60
|
+
crew_instance = crew.crew()
|
|
61
|
+
result = crew_instance.kickoff(
|
|
62
|
+
inputs={
|
|
63
|
+
"title": self.state.task,
|
|
64
|
+
"body": self.state.commit_message or "",
|
|
65
|
+
"conversation_history": history or "No conversation yet",
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
response_text = str(result) if result else "No response"
|
|
70
|
+
self._github_manager.add_comment(self.state.issue_id, response_text)
|
|
71
|
+
|
|
72
|
+
self.state.messages.append(
|
|
73
|
+
ConversationMessage(
|
|
74
|
+
author="llm",
|
|
75
|
+
content=response_text,
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
self.state.stage = ConversationStage.SPEC_ELCITATION
|
|
79
|
+
|
|
80
|
+
@listen(initialize_conversation)
|
|
81
|
+
def handle_new_comment(self, input_data: NewCommentInput):
|
|
82
|
+
"""Handle new comment from user."""
|
|
83
|
+
logger.info(f"💬 New comment from {input_data.author}")
|
|
84
|
+
|
|
85
|
+
self.state.messages.append(
|
|
86
|
+
ConversationMessage(
|
|
87
|
+
author=input_data.author,
|
|
88
|
+
content=input_data.content,
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if input_data.thumbs_up:
|
|
93
|
+
logger.info("👍 Thumbs up detected, moving to next stage")
|
|
94
|
+
self._handle_approval()
|
|
95
|
+
else:
|
|
96
|
+
self._ask_specification_question()
|
|
97
|
+
|
|
98
|
+
@listen(handle_new_comment)
|
|
99
|
+
def handle_reaction(self, input_data: ReactionInput):
|
|
100
|
+
"""Handle thumbs up reaction."""
|
|
101
|
+
if input_data.reaction == "+1":
|
|
102
|
+
logger.info("👍 Thumbs up reaction detected")
|
|
103
|
+
self.state.thumbs_up_given = True
|
|
104
|
+
self._handle_approval()
|
|
105
|
+
|
|
106
|
+
def _handle_approval(self):
|
|
107
|
+
"""Handle specification approval and move to story planning."""
|
|
108
|
+
if self.state.stage == ConversationStage.SPEC_ELCITATION:
|
|
109
|
+
logger.info("✅ Specification approved, moving to story breakdown")
|
|
110
|
+
self.state.stage = ConversationStage.SPEC_APPROVAL
|
|
111
|
+
self._break_down_stories()
|
|
112
|
+
elif self.state.stage == ConversationStage.STORY_BREAKDOWN:
|
|
113
|
+
logger.info("✅ Stories approved, initializing Ralph loop")
|
|
114
|
+
self.state.stage = ConversationStage.STORY_APPROVAL
|
|
115
|
+
self._init_ralph_loop()
|
|
116
|
+
|
|
117
|
+
def _break_down_stories(self):
|
|
118
|
+
"""Break down approved specification into stories."""
|
|
119
|
+
if not self.conversation_crew:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
crew = self.conversation_crew
|
|
123
|
+
spec = self.state.specification or self.state.messages[-1].content
|
|
124
|
+
|
|
125
|
+
crew_instance = crew.crew()
|
|
126
|
+
result = crew_instance.kickoff(
|
|
127
|
+
inputs={
|
|
128
|
+
"specification": spec,
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
crew_output = result
|
|
133
|
+
if hasattr(result, "result"):
|
|
134
|
+
crew_output = result.result # pyright: ignore
|
|
135
|
+
|
|
136
|
+
stories_text = "## 📋 Proposed Stories\n\n"
|
|
137
|
+
|
|
138
|
+
stories: List[Story] = crew_output.pydantic # type: ignore[assignment]
|
|
139
|
+
|
|
140
|
+
for story in stories:
|
|
141
|
+
self.state.stories.append(story)
|
|
142
|
+
stories_text += f" - [{' ' if story.completed else 'x'}] {story.description}\n\n---\n\n"
|
|
143
|
+
|
|
144
|
+
stories_text += "\nPlease review these stories. **Add 👍 to this comment when you approve the plan.**"
|
|
145
|
+
|
|
146
|
+
self._github_manager.add_comment(self.state.issue_id, stories_text)
|
|
147
|
+
self.state.specification = spec
|
|
148
|
+
self.state.stage = ConversationStage.STORY_BREAKDOWN
|
|
149
|
+
|
|
150
|
+
def _init_ralph_loop(self):
|
|
151
|
+
"""Initialize Ralph loop with approved stories."""
|
|
152
|
+
if not self.state.stories:
|
|
153
|
+
logger.warning("No stories to execute")
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
next_story = None
|
|
157
|
+
for story in self.state.stories:
|
|
158
|
+
if not story.completed:
|
|
159
|
+
next_story = story
|
|
160
|
+
self.state.approved_story_id = story.id
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
if not next_story:
|
|
164
|
+
logger.info("All stories completed!")
|
|
165
|
+
self.state.stage = ConversationStage.COMPLETED
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
logger.info(f"🚀 Initializing Ralph for story: {next_story.title}")
|
|
169
|
+
|
|
170
|
+
conversation_summary = "\n".join(f"- {msg.author}: {msg.content}" for msg in self.state.messages[-5:])
|
|
171
|
+
|
|
172
|
+
if self.conversation_crew:
|
|
173
|
+
crew_instance = self.conversation_crew.crew()
|
|
174
|
+
result = crew_instance.kickoff(
|
|
175
|
+
inputs={
|
|
176
|
+
"specification": self.state.specification,
|
|
177
|
+
"story": next_story.model_dump(),
|
|
178
|
+
"conversation_summary": conversation_summary,
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
response_text = str(result) if result else "No response"
|
|
183
|
+
brief = (
|
|
184
|
+
f"## 🚀 Starting Development\n\n"
|
|
185
|
+
f"**Story:** {next_story.title}\n\n"
|
|
186
|
+
f"---\n\n"
|
|
187
|
+
f"{response_text}\n\n"
|
|
188
|
+
f"Development in progress..."
|
|
189
|
+
)
|
|
190
|
+
self._github_manager.add_comment(self.state.issue_id, brief)
|
|
191
|
+
|
|
192
|
+
logger.info("Transitioning to Ralph execution phase")
|
|
193
|
+
self.state.stage = ConversationStage.RALPH_INIT
|
|
194
|
+
|
|
195
|
+
def check_ralph_completion(self):
|
|
196
|
+
"""Check if Ralph loop completed the current story."""
|
|
197
|
+
if self.state.build_success and self.state.test_success:
|
|
198
|
+
logger.info("✅ Story completed successfully")
|
|
199
|
+
|
|
200
|
+
for story in self.state.stories:
|
|
201
|
+
if story.id == self.state.approved_story_id:
|
|
202
|
+
story.completed = True
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
self._github_manager.add_comment(
|
|
206
|
+
self.state.issue_id,
|
|
207
|
+
f"## ✅ Story Completed\n\n"
|
|
208
|
+
f"Story {self.state.approved_story_id} completed successfully.\n\n"
|
|
209
|
+
f"Review the changes and provide feedback.",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
self._break_down_stories()
|
|
213
|
+
elif self.state.stage == ConversationStage.RALPH_EXECUTION:
|
|
214
|
+
logger.info("⏳ Ralph execution in progress")
|
|
215
|
+
return "wait"
|
|
216
|
+
else:
|
|
217
|
+
logger.error("❌ Ralph execution failed")
|
|
218
|
+
return "error"
|
|
219
|
+
|
|
220
|
+
@listen("wait")
|
|
221
|
+
def wait_for_completion(self):
|
|
222
|
+
"""Wait for Ralph to complete."""
|
|
223
|
+
return "continue"
|
|
224
|
+
|
|
225
|
+
@listen("error")
|
|
226
|
+
def handle_ralph_error(self):
|
|
227
|
+
"""Handle Ralph execution errors."""
|
|
228
|
+
logger.error("Ralph execution error, requesting feedback")
|
|
229
|
+
self._github_manager.add_comment(
|
|
230
|
+
self.state.issue_id,
|
|
231
|
+
"## ❌ Error\n\nDevelopment encountered an error. Please review and provide guidance.",
|
|
232
|
+
)
|
|
233
|
+
self.state.stage = ConversationStage.INIT
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Conversation-driven specification flow models."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import TYPE_CHECKING, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from flows.base import BaseFlowModel
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from crews.plan_crew.types import Story
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConversationStage(str, Enum):
|
|
15
|
+
"""Stages of the conversation flow."""
|
|
16
|
+
|
|
17
|
+
INIT = "init"
|
|
18
|
+
SPEC_ELCITATION = "spec_elicitation"
|
|
19
|
+
SPEC_APPROVAL = "spec_approval"
|
|
20
|
+
STORY_BREAKDOWN = "story_breakdown"
|
|
21
|
+
STORY_APPROVAL = "story_approval"
|
|
22
|
+
RALPH_INIT = "ralph_init"
|
|
23
|
+
RALPH_EXECUTION = "ralph_execution"
|
|
24
|
+
COMPLETED = "completed"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ConversationMessage(BaseModel):
|
|
28
|
+
"""A message in the conversation."""
|
|
29
|
+
|
|
30
|
+
author: str = Field(description="Message author ('user' or 'llm')")
|
|
31
|
+
content: str = Field(description="Message content")
|
|
32
|
+
timestamp: Optional[str] = Field(default=None, description="ISO timestamp")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class NewCommentInput(BaseModel):
|
|
36
|
+
"""Input when a new comment is received."""
|
|
37
|
+
|
|
38
|
+
comment_id: int
|
|
39
|
+
author: str
|
|
40
|
+
content: str
|
|
41
|
+
thumbs_up: bool = False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ReactionInput(BaseModel):
|
|
45
|
+
"""Input when a thumbs up reaction is detected."""
|
|
46
|
+
|
|
47
|
+
comment_id: int
|
|
48
|
+
reaction: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ConversationFlowState(BaseFlowModel):
|
|
52
|
+
"""State for conversation flow."""
|
|
53
|
+
|
|
54
|
+
stage: ConversationStage = Field(default=ConversationStage.INIT)
|
|
55
|
+
messages: list[ConversationMessage] = Field(default_factory=list, description="Conversation history")
|
|
56
|
+
specification: Optional[str] = Field(default=None, description="Final approved specification")
|
|
57
|
+
requirements: list[str] = Field(default_factory=list)
|
|
58
|
+
stories: list["Story"] = Field(default_factory=list, description="User stories")
|
|
59
|
+
approved_story_id: Optional[int] = Field(default=None, description="ID of story to execute next")
|
|
60
|
+
ralph_output: Optional[str] = Field(default=None, description="Ralph agent output")
|
|
61
|
+
build_success: bool = Field(default=False)
|
|
62
|
+
test_success: bool = Field(default=False)
|
|
63
|
+
last_comment_id: Optional[int] = Field(default=None, description="Last comment ID we posted")
|
|
64
|
+
thumbs_up_given: bool = Field(default=False, description="User gave thumbs up")
|