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.
@@ -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")