titan-cli 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.
- titan_cli/__init__.py +3 -0
- titan_cli/__main__.py +4 -0
- titan_cli/ai/__init__.py +0 -0
- titan_cli/ai/agents/__init__.py +15 -0
- titan_cli/ai/agents/base.py +152 -0
- titan_cli/ai/client.py +170 -0
- titan_cli/ai/constants.py +56 -0
- titan_cli/ai/exceptions.py +48 -0
- titan_cli/ai/models.py +34 -0
- titan_cli/ai/oauth_helper.py +120 -0
- titan_cli/ai/providers/__init__.py +9 -0
- titan_cli/ai/providers/anthropic.py +117 -0
- titan_cli/ai/providers/base.py +75 -0
- titan_cli/ai/providers/gemini.py +278 -0
- titan_cli/cli.py +59 -0
- titan_cli/clients/__init__.py +1 -0
- titan_cli/clients/gcloud_client.py +52 -0
- titan_cli/core/__init__.py +3 -0
- titan_cli/core/config.py +274 -0
- titan_cli/core/discovery.py +51 -0
- titan_cli/core/errors.py +81 -0
- titan_cli/core/models.py +52 -0
- titan_cli/core/plugins/available.py +36 -0
- titan_cli/core/plugins/models.py +67 -0
- titan_cli/core/plugins/plugin_base.py +108 -0
- titan_cli/core/plugins/plugin_registry.py +163 -0
- titan_cli/core/secrets.py +141 -0
- titan_cli/core/workflows/__init__.py +22 -0
- titan_cli/core/workflows/models.py +88 -0
- titan_cli/core/workflows/project_step_source.py +86 -0
- titan_cli/core/workflows/workflow_exceptions.py +17 -0
- titan_cli/core/workflows/workflow_filter_service.py +137 -0
- titan_cli/core/workflows/workflow_registry.py +419 -0
- titan_cli/core/workflows/workflow_sources.py +307 -0
- titan_cli/engine/__init__.py +39 -0
- titan_cli/engine/builder.py +159 -0
- titan_cli/engine/context.py +82 -0
- titan_cli/engine/mock_context.py +176 -0
- titan_cli/engine/results.py +91 -0
- titan_cli/engine/steps/ai_assistant_step.py +185 -0
- titan_cli/engine/steps/command_step.py +93 -0
- titan_cli/engine/utils/__init__.py +3 -0
- titan_cli/engine/utils/venv.py +31 -0
- titan_cli/engine/workflow_executor.py +187 -0
- titan_cli/external_cli/__init__.py +0 -0
- titan_cli/external_cli/configs.py +17 -0
- titan_cli/external_cli/launcher.py +65 -0
- titan_cli/messages.py +121 -0
- titan_cli/ui/tui/__init__.py +205 -0
- titan_cli/ui/tui/__previews__/statusbar_preview.py +88 -0
- titan_cli/ui/tui/app.py +113 -0
- titan_cli/ui/tui/icons.py +70 -0
- titan_cli/ui/tui/screens/__init__.py +24 -0
- titan_cli/ui/tui/screens/ai_config.py +498 -0
- titan_cli/ui/tui/screens/ai_config_wizard.py +882 -0
- titan_cli/ui/tui/screens/base.py +110 -0
- titan_cli/ui/tui/screens/cli_launcher.py +151 -0
- titan_cli/ui/tui/screens/global_setup_wizard.py +363 -0
- titan_cli/ui/tui/screens/main_menu.py +162 -0
- titan_cli/ui/tui/screens/plugin_config_wizard.py +550 -0
- titan_cli/ui/tui/screens/plugin_management.py +377 -0
- titan_cli/ui/tui/screens/project_setup_wizard.py +686 -0
- titan_cli/ui/tui/screens/workflow_execution.py +592 -0
- titan_cli/ui/tui/screens/workflows.py +249 -0
- titan_cli/ui/tui/textual_components.py +537 -0
- titan_cli/ui/tui/textual_workflow_executor.py +405 -0
- titan_cli/ui/tui/theme.py +102 -0
- titan_cli/ui/tui/widgets/__init__.py +40 -0
- titan_cli/ui/tui/widgets/button.py +108 -0
- titan_cli/ui/tui/widgets/header.py +116 -0
- titan_cli/ui/tui/widgets/panel.py +81 -0
- titan_cli/ui/tui/widgets/status_bar.py +115 -0
- titan_cli/ui/tui/widgets/table.py +77 -0
- titan_cli/ui/tui/widgets/text.py +177 -0
- titan_cli/utils/__init__.py +0 -0
- titan_cli/utils/autoupdate.py +155 -0
- titan_cli-0.1.0.dist-info/METADATA +149 -0
- titan_cli-0.1.0.dist-info/RECORD +146 -0
- titan_cli-0.1.0.dist-info/WHEEL +4 -0
- titan_cli-0.1.0.dist-info/entry_points.txt +9 -0
- titan_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- titan_plugin_git/__init__.py +1 -0
- titan_plugin_git/clients/__init__.py +8 -0
- titan_plugin_git/clients/git_client.py +772 -0
- titan_plugin_git/exceptions.py +40 -0
- titan_plugin_git/messages.py +112 -0
- titan_plugin_git/models.py +39 -0
- titan_plugin_git/plugin.py +118 -0
- titan_plugin_git/steps/__init__.py +1 -0
- titan_plugin_git/steps/ai_commit_message_step.py +171 -0
- titan_plugin_git/steps/branch_steps.py +104 -0
- titan_plugin_git/steps/commit_step.py +80 -0
- titan_plugin_git/steps/push_step.py +63 -0
- titan_plugin_git/steps/status_step.py +59 -0
- titan_plugin_git/workflows/__previews__/__init__.py +1 -0
- titan_plugin_git/workflows/__previews__/commit_ai_preview.py +124 -0
- titan_plugin_git/workflows/commit-ai.yaml +28 -0
- titan_plugin_github/__init__.py +11 -0
- titan_plugin_github/agents/__init__.py +6 -0
- titan_plugin_github/agents/config_loader.py +130 -0
- titan_plugin_github/agents/issue_generator.py +353 -0
- titan_plugin_github/agents/pr_agent.py +528 -0
- titan_plugin_github/clients/__init__.py +8 -0
- titan_plugin_github/clients/github_client.py +1105 -0
- titan_plugin_github/config/__init__.py +0 -0
- titan_plugin_github/config/pr_agent.toml +85 -0
- titan_plugin_github/exceptions.py +28 -0
- titan_plugin_github/messages.py +88 -0
- titan_plugin_github/models.py +330 -0
- titan_plugin_github/plugin.py +131 -0
- titan_plugin_github/steps/__init__.py +12 -0
- titan_plugin_github/steps/ai_pr_step.py +172 -0
- titan_plugin_github/steps/create_pr_step.py +86 -0
- titan_plugin_github/steps/github_prompt_steps.py +171 -0
- titan_plugin_github/steps/issue_steps.py +143 -0
- titan_plugin_github/steps/preview_step.py +40 -0
- titan_plugin_github/utils.py +82 -0
- titan_plugin_github/workflows/__previews__/__init__.py +1 -0
- titan_plugin_github/workflows/__previews__/create_pr_ai_preview.py +140 -0
- titan_plugin_github/workflows/create-issue-ai.yaml +32 -0
- titan_plugin_github/workflows/create-pr-ai.yaml +49 -0
- titan_plugin_jira/__init__.py +8 -0
- titan_plugin_jira/agents/__init__.py +6 -0
- titan_plugin_jira/agents/config_loader.py +154 -0
- titan_plugin_jira/agents/jira_agent.py +553 -0
- titan_plugin_jira/agents/prompts.py +364 -0
- titan_plugin_jira/agents/response_parser.py +435 -0
- titan_plugin_jira/agents/token_tracker.py +223 -0
- titan_plugin_jira/agents/validators.py +246 -0
- titan_plugin_jira/clients/jira_client.py +745 -0
- titan_plugin_jira/config/jira_agent.toml +92 -0
- titan_plugin_jira/config/templates/issue_analysis.md.j2 +78 -0
- titan_plugin_jira/exceptions.py +37 -0
- titan_plugin_jira/formatters/__init__.py +6 -0
- titan_plugin_jira/formatters/markdown_formatter.py +245 -0
- titan_plugin_jira/messages.py +115 -0
- titan_plugin_jira/models.py +89 -0
- titan_plugin_jira/plugin.py +264 -0
- titan_plugin_jira/steps/ai_analyze_issue_step.py +105 -0
- titan_plugin_jira/steps/get_issue_step.py +82 -0
- titan_plugin_jira/steps/prompt_select_issue_step.py +80 -0
- titan_plugin_jira/steps/search_saved_query_step.py +238 -0
- titan_plugin_jira/utils/__init__.py +13 -0
- titan_plugin_jira/utils/issue_sorter.py +140 -0
- titan_plugin_jira/utils/saved_queries.py +150 -0
- titan_plugin_jira/workflows/analyze-jira-issues.yaml +34 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Dict, Optional, Tuple
|
|
3
|
+
import re
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from titan_cli.ai.agents.base import BaseAIAgent, AgentRequest, AIGenerator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class IssueSizeEstimation:
|
|
11
|
+
"""
|
|
12
|
+
Issue size estimation with token limits.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
complexity: Size category (simple, moderate, complex, very_complex)
|
|
16
|
+
max_tokens: Maximum tokens for issue description generation
|
|
17
|
+
description_length: Length of user's description
|
|
18
|
+
"""
|
|
19
|
+
complexity: str
|
|
20
|
+
max_tokens: int
|
|
21
|
+
description_length: int
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class IssueGeneratorAgent(BaseAIAgent):
|
|
25
|
+
def __init__(self, ai_client: AIGenerator, template_dir: Optional[Path] = None):
|
|
26
|
+
super().__init__(ai_client)
|
|
27
|
+
self.template_dir = template_dir or Path(".github/ISSUE_TEMPLATE")
|
|
28
|
+
self.max_tokens = 8192 # Add token limit configuration
|
|
29
|
+
self.categories = {
|
|
30
|
+
"feature": {
|
|
31
|
+
"template": "feature.md",
|
|
32
|
+
"labels": ["feature"],
|
|
33
|
+
"prefix": "feat"
|
|
34
|
+
},
|
|
35
|
+
"improvement": {
|
|
36
|
+
"template": "improvement.md",
|
|
37
|
+
"labels": ["improvement"],
|
|
38
|
+
"prefix": "improve"
|
|
39
|
+
},
|
|
40
|
+
"bug": {
|
|
41
|
+
"template": "bug.md",
|
|
42
|
+
"labels": ["bug"],
|
|
43
|
+
"prefix": "fix"
|
|
44
|
+
},
|
|
45
|
+
"refactor": {
|
|
46
|
+
"template": "refactor.md",
|
|
47
|
+
"labels": ["refactor", "technical-debt"],
|
|
48
|
+
"prefix": "refactor"
|
|
49
|
+
},
|
|
50
|
+
"chore": {
|
|
51
|
+
"template": "chore.md",
|
|
52
|
+
"labels": ["chore", "maintenance"],
|
|
53
|
+
"prefix": "chore"
|
|
54
|
+
},
|
|
55
|
+
"documentation": {
|
|
56
|
+
"template": "documentation.md",
|
|
57
|
+
"labels": ["documentation"],
|
|
58
|
+
"prefix": "docs"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Label aliases for mapping to different repository label conventions
|
|
63
|
+
self.label_aliases = {
|
|
64
|
+
"feature": ["feature", "enhancement", "new-feature", "feat"],
|
|
65
|
+
"improvement": ["improvement", "enhancement", "optimization", "perf"],
|
|
66
|
+
"bug": ["bug", "fix", "defect", "error"],
|
|
67
|
+
"refactor": ["refactor", "refactoring", "technical-debt", "tech-debt"],
|
|
68
|
+
"chore": ["chore", "maintenance", "housekeeping"],
|
|
69
|
+
"documentation": ["documentation", "docs", "doc"]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def get_system_prompt(self) -> str:
|
|
73
|
+
return """You are an expert at creating highly professional, descriptive, and useful GitHub issues.
|
|
74
|
+
Your task is to:
|
|
75
|
+
1. Analyze the user's description and categorize it
|
|
76
|
+
2. Generate an issue title and detailed description following the appropriate template (if available)
|
|
77
|
+
3. Ensure the title follows the Conventional Commits specification (e.g., "feat(scope): brief description")
|
|
78
|
+
4. Use English for all content
|
|
79
|
+
5. Prioritize clarity, conciseness, and actionable detail
|
|
80
|
+
6. Preserve any code snippets exactly as provided, formatted in markdown code blocks
|
|
81
|
+
7. Always return your response as valid JSON format for reliable parsing
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def _load_template(self, template_name: str) -> Optional[str]:
|
|
85
|
+
"""
|
|
86
|
+
Load a template file from the template directory.
|
|
87
|
+
Returns None if template doesn't exist or can't be read.
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
template_path = self.template_dir / template_name
|
|
91
|
+
if template_path.exists() and template_path.is_file():
|
|
92
|
+
return template_path.read_text(encoding="utf-8")
|
|
93
|
+
except (IOError, FileNotFoundError, PermissionError, OSError):
|
|
94
|
+
pass
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def _load_all_templates(self) -> Dict[str, Optional[str]]:
|
|
98
|
+
"""
|
|
99
|
+
Load all available templates.
|
|
100
|
+
Returns a dict mapping category name to template content.
|
|
101
|
+
"""
|
|
102
|
+
templates = {}
|
|
103
|
+
for category, info in self.categories.items():
|
|
104
|
+
template_content = self._load_template(info["template"])
|
|
105
|
+
templates[category] = template_content
|
|
106
|
+
return templates
|
|
107
|
+
|
|
108
|
+
def _estimate_issue_complexity(self, description: str) -> IssueSizeEstimation:
|
|
109
|
+
"""
|
|
110
|
+
Estimate issue complexity based on description length and content.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
description: User's issue description
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
IssueSizeEstimation with complexity and token limits
|
|
117
|
+
"""
|
|
118
|
+
desc_length = len(description)
|
|
119
|
+
# Count newlines as indicator of detail
|
|
120
|
+
lines = description.count('\n') + 1
|
|
121
|
+
# Count code blocks as indicator of technical complexity
|
|
122
|
+
code_blocks = description.count('```')
|
|
123
|
+
|
|
124
|
+
# Determine complexity and set appropriate token limits
|
|
125
|
+
if desc_length < 200 and lines <= 3 and code_blocks == 0:
|
|
126
|
+
# Simple issue: brief request, quick fix
|
|
127
|
+
complexity = "simple"
|
|
128
|
+
max_tokens = 3000 # Increased to ensure complete JSON
|
|
129
|
+
elif desc_length < 500 and lines <= 10:
|
|
130
|
+
# Moderate issue: standard feature or bug with some detail
|
|
131
|
+
complexity = "moderate"
|
|
132
|
+
max_tokens = 4000 # Increased to ensure complete JSON
|
|
133
|
+
elif desc_length < 1000 and lines <= 25:
|
|
134
|
+
# Complex issue: detailed requirements, multiple aspects
|
|
135
|
+
complexity = "complex"
|
|
136
|
+
max_tokens = 6000 # Increased to ensure complete JSON
|
|
137
|
+
else:
|
|
138
|
+
# Very complex: comprehensive spec, architectural changes
|
|
139
|
+
complexity = "very_complex"
|
|
140
|
+
max_tokens = 8000 # Increased to ensure complete JSON
|
|
141
|
+
|
|
142
|
+
return IssueSizeEstimation(
|
|
143
|
+
complexity=complexity,
|
|
144
|
+
max_tokens=max_tokens,
|
|
145
|
+
description_length=desc_length
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def _parse_ai_response(self, content: str) -> Tuple[str, str, str]:
|
|
149
|
+
"""
|
|
150
|
+
Parse AI response to extract category, title, and body.
|
|
151
|
+
Tries JSON parsing first, falls back to regex for robustness.
|
|
152
|
+
Handles incomplete JSON by attempting to fix it.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Tuple of (category, title, body)
|
|
156
|
+
"""
|
|
157
|
+
# Try JSON parsing first (more robust, avoids conflicts with user text)
|
|
158
|
+
try:
|
|
159
|
+
# Remove ONLY the outer markdown code block wrapper (```json ... ```)
|
|
160
|
+
# but preserve code blocks inside the JSON body content
|
|
161
|
+
cleaned_content = content.strip()
|
|
162
|
+
|
|
163
|
+
# Remove opening ```json or ``` if present at the start
|
|
164
|
+
if cleaned_content.startswith('```json'):
|
|
165
|
+
cleaned_content = cleaned_content[7:].lstrip()
|
|
166
|
+
elif cleaned_content.startswith('```'):
|
|
167
|
+
cleaned_content = cleaned_content[3:].lstrip()
|
|
168
|
+
|
|
169
|
+
# Remove closing ``` if present at the end
|
|
170
|
+
if cleaned_content.endswith('```'):
|
|
171
|
+
cleaned_content = cleaned_content[:-3].rstrip()
|
|
172
|
+
|
|
173
|
+
# Try to find JSON in the content (handle incomplete JSON)
|
|
174
|
+
json_match = re.search(r'\{[\s\S]*\}', cleaned_content)
|
|
175
|
+
|
|
176
|
+
# If no complete JSON found, try to fix incomplete JSON
|
|
177
|
+
if not json_match and cleaned_content.strip().startswith('{'):
|
|
178
|
+
# JSON might be incomplete (missing closing quote and brace)
|
|
179
|
+
json_str = cleaned_content.strip()
|
|
180
|
+
# Try to close the JSON properly
|
|
181
|
+
if not json_str.endswith('}'):
|
|
182
|
+
# Close any unclosed string first
|
|
183
|
+
if json_str.count('"') % 2 != 0:
|
|
184
|
+
json_str = json_str + '"'
|
|
185
|
+
# Then close the JSON object
|
|
186
|
+
json_str = json_str + '\n}'
|
|
187
|
+
elif json_match:
|
|
188
|
+
json_str = json_match.group(0)
|
|
189
|
+
else:
|
|
190
|
+
json_str = None
|
|
191
|
+
|
|
192
|
+
if json_str:
|
|
193
|
+
data = json.loads(json_str)
|
|
194
|
+
|
|
195
|
+
category = data.get("category", "feature").lower()
|
|
196
|
+
title = data.get("title", "New issue")
|
|
197
|
+
body = data.get("body", "")
|
|
198
|
+
|
|
199
|
+
# Validate category
|
|
200
|
+
if category not in self.categories:
|
|
201
|
+
category = "feature"
|
|
202
|
+
|
|
203
|
+
return category, title, body
|
|
204
|
+
except (json.JSONDecodeError, AttributeError):
|
|
205
|
+
# JSON parsing failed, fall back to regex
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
# Fallback: regex parsing for backwards compatibility
|
|
209
|
+
# Extract category
|
|
210
|
+
category_match = re.search(r'CATEGORY:\s*(\w+)', content, re.IGNORECASE)
|
|
211
|
+
category = category_match.group(1).strip().lower() if category_match else "feature"
|
|
212
|
+
|
|
213
|
+
# Validate category
|
|
214
|
+
if category not in self.categories:
|
|
215
|
+
category = "feature"
|
|
216
|
+
|
|
217
|
+
# Extract title
|
|
218
|
+
title_match = re.search(r'TITLE:\s*(.+?)(?=\n|DESCRIPTION:|$)', content, re.IGNORECASE)
|
|
219
|
+
title = title_match.group(1).strip() if title_match else "New issue"
|
|
220
|
+
|
|
221
|
+
# Extract description/body
|
|
222
|
+
desc_match = re.search(r'DESCRIPTION:\s*(.+)', content, re.IGNORECASE | re.DOTALL)
|
|
223
|
+
body = desc_match.group(1).strip() if desc_match else content
|
|
224
|
+
|
|
225
|
+
return category, title, body
|
|
226
|
+
|
|
227
|
+
def _map_labels_to_available(self, category: str, available_labels: Optional[list] = None) -> list:
|
|
228
|
+
"""
|
|
229
|
+
Map category labels to available repository labels using aliases.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
category: The issue category
|
|
233
|
+
available_labels: List of labels available in the repository (optional)
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
List of labels that exist in the repository, or default labels if no filtering
|
|
237
|
+
"""
|
|
238
|
+
default_labels = self.categories.get(category, self.categories["feature"])["labels"]
|
|
239
|
+
|
|
240
|
+
# If no available_labels provided, return defaults (no filtering)
|
|
241
|
+
if available_labels is None:
|
|
242
|
+
return default_labels
|
|
243
|
+
|
|
244
|
+
# Get aliases for this category
|
|
245
|
+
aliases = self.label_aliases.get(category, [])
|
|
246
|
+
|
|
247
|
+
# Find matching labels in the repository
|
|
248
|
+
matched_labels = []
|
|
249
|
+
for alias in aliases:
|
|
250
|
+
# Case-insensitive matching
|
|
251
|
+
if alias.lower() in [label.lower() for label in available_labels]:
|
|
252
|
+
# Find the exact case from available_labels
|
|
253
|
+
matched_label = next(label for label in available_labels if label.lower() == alias.lower())
|
|
254
|
+
if matched_label not in matched_labels:
|
|
255
|
+
matched_labels.append(matched_label)
|
|
256
|
+
|
|
257
|
+
# Fallback: if no matches found, return empty list (graceful degradation)
|
|
258
|
+
return matched_labels
|
|
259
|
+
|
|
260
|
+
def generate_issue(self, user_description: str, available_labels: Optional[list] = None) -> Dict[str, any]:
|
|
261
|
+
"""
|
|
262
|
+
Generate a complete issue with auto-categorization in a single AI call.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
dict with keys: title, body, category, labels, template_used, tokens_used, complexity
|
|
266
|
+
"""
|
|
267
|
+
# Estimate issue complexity to determine appropriate token allocation
|
|
268
|
+
estimation = self._estimate_issue_complexity(user_description)
|
|
269
|
+
|
|
270
|
+
# Load all available templates
|
|
271
|
+
all_templates = self._load_all_templates()
|
|
272
|
+
|
|
273
|
+
# Build prompt that includes all templates and asks AI to categorize + generate
|
|
274
|
+
templates_section = self._format_templates_for_prompt(all_templates)
|
|
275
|
+
|
|
276
|
+
prompt = f"""
|
|
277
|
+
Analyze the following issue description and:
|
|
278
|
+
1. Categorize it into ONE category: feature, improvement, bug, refactor, chore, or documentation
|
|
279
|
+
2. Generate a complete GitHub issue following the appropriate template
|
|
280
|
+
|
|
281
|
+
User description:
|
|
282
|
+
---
|
|
283
|
+
{user_description}
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
{templates_section}
|
|
287
|
+
|
|
288
|
+
Instructions:
|
|
289
|
+
- Choose the most appropriate category based on the description
|
|
290
|
+
- Follow the template structure for that category (if available)
|
|
291
|
+
- Remove all HTML comments (<!-- -->)
|
|
292
|
+
- Keep only section headers (##) that have meaningful content
|
|
293
|
+
- IMPORTANT: Completely omit optional sections (marked with "Optional") if they don't apply or have no meaningful content
|
|
294
|
+
- Never include a section header followed by just a language identifier (e.g., "python", "yaml") without actual code
|
|
295
|
+
- Fill in meaningful content for each section you include
|
|
296
|
+
- Preserve any code snippets exactly as provided in markdown code blocks
|
|
297
|
+
- Use the correct conventional commit prefix for the title
|
|
298
|
+
- Adjust detail level based on issue complexity ({estimation.complexity}):
|
|
299
|
+
* Simple: Brief, direct descriptions (1-2 sentences per section)
|
|
300
|
+
* Moderate: Standard detail (2-3 sentences per section)
|
|
301
|
+
* Complex: Comprehensive detail with examples
|
|
302
|
+
* Very Complex: Full context, edge cases, and implementation notes
|
|
303
|
+
|
|
304
|
+
Output format (REQUIRED - JSON):
|
|
305
|
+
{{
|
|
306
|
+
"category": "<category>",
|
|
307
|
+
"title": "<prefix>(scope): brief description",
|
|
308
|
+
"body": "<complete markdown-formatted description>"
|
|
309
|
+
}}
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
# Single AI call for categorization + generation with appropriate token limit
|
|
313
|
+
request = AgentRequest(
|
|
314
|
+
context=prompt,
|
|
315
|
+
max_tokens=estimation.max_tokens
|
|
316
|
+
)
|
|
317
|
+
response = self.generate(request)
|
|
318
|
+
|
|
319
|
+
# Parse response using robust regex parsing
|
|
320
|
+
category, title, body = self._parse_ai_response(response.content)
|
|
321
|
+
|
|
322
|
+
template_used = all_templates.get(category) is not None
|
|
323
|
+
|
|
324
|
+
# Map labels to available repository labels (with fallback to empty list)
|
|
325
|
+
labels = self._map_labels_to_available(category, available_labels)
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
"title": title,
|
|
329
|
+
"body": body,
|
|
330
|
+
"category": category,
|
|
331
|
+
"labels": labels,
|
|
332
|
+
"template_used": template_used,
|
|
333
|
+
"tokens_used": response.tokens_used,
|
|
334
|
+
"complexity": estimation.complexity
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
def _format_templates_for_prompt(self, templates: Dict[str, Optional[str]]) -> str:
|
|
338
|
+
"""
|
|
339
|
+
Format all templates into a string for the AI prompt.
|
|
340
|
+
"""
|
|
341
|
+
if not any(templates.values()):
|
|
342
|
+
return "No templates available. Generate structured content based on category best practices."
|
|
343
|
+
|
|
344
|
+
formatted = "Available templates:\n\n"
|
|
345
|
+
for category, template_content in templates.items():
|
|
346
|
+
if template_content:
|
|
347
|
+
category_info = self.categories[category]
|
|
348
|
+
formatted += f"### {category.upper()} (prefix: {category_info['prefix']})\n"
|
|
349
|
+
formatted += f"```\n{template_content}\n```\n\n"
|
|
350
|
+
else:
|
|
351
|
+
formatted += f"### {category.upper()} (no template available)\n\n"
|
|
352
|
+
|
|
353
|
+
return formatted
|