103 lines
3.6 KiB
Python
103 lines
3.6 KiB
Python
import json
|
|
import ai_client
|
|
import mma_prompts
|
|
import re
|
|
|
|
def generate_tickets(track_brief: str, module_skeletons: str) -> list[dict]:
|
|
"""
|
|
Tier 2 (Tech Lead) call.
|
|
Breaks down a Track Brief and module skeletons into discrete Tier 3 Tickets.
|
|
"""
|
|
# 1. Set Tier 2 Model (Tech Lead - Flash)
|
|
ai_client.set_provider('gemini', 'gemini-2.5-flash-lite')
|
|
ai_client.reset_session()
|
|
|
|
# 2. Construct Prompt
|
|
system_prompt = mma_prompts.PROMPTS.get("tier2_sprint_planning")
|
|
|
|
user_message = (
|
|
f"### TRACK BRIEF:\n{track_brief}\n\n"
|
|
f"### MODULE SKELETONS:\n{module_skeletons}\n\n"
|
|
"Please generate the implementation tickets for this track."
|
|
)
|
|
|
|
# Set custom system prompt for this call
|
|
old_system_prompt = ai_client._custom_system_prompt
|
|
ai_client.set_custom_system_prompt(system_prompt)
|
|
|
|
try:
|
|
# 3. Call Tier 2 Model
|
|
response = ai_client.send(
|
|
md_content="",
|
|
user_message=user_message
|
|
)
|
|
|
|
# 4. Parse JSON Output
|
|
# Extract JSON array from markdown code blocks if present
|
|
json_match = response.strip()
|
|
if "```json" in json_match:
|
|
json_match = json_match.split("```json")[1].split("```")[0].strip()
|
|
elif "```" in json_match:
|
|
json_match = json_match.split("```")[1].split("```")[0].strip()
|
|
|
|
# If it's still not valid JSON, try to find a [ ... ] block
|
|
if not (json_match.startswith('[') and json_match.endswith(']')):
|
|
match = re.search(r'\[\s*\{.*\}\s*\]', json_match, re.DOTALL)
|
|
if match:
|
|
json_match = match.group(0)
|
|
|
|
tickets = json.loads(json_match)
|
|
return tickets
|
|
except Exception as e:
|
|
print(f"Error parsing Tier 2 response: {e}")
|
|
# print(f"Raw response: {response}")
|
|
return []
|
|
finally:
|
|
# Restore old system prompt
|
|
ai_client.set_custom_system_prompt(old_system_prompt)
|
|
|
|
def topological_sort(tickets: list[dict]) -> list[dict]:
|
|
"""
|
|
Sorts a list of tickets based on their 'depends_on' field.
|
|
Raises ValueError if a circular dependency or missing internal dependency is detected.
|
|
"""
|
|
# 1. Map ID to ticket and build graph
|
|
ticket_map = {t['id']: t for t in tickets}
|
|
adj = {t['id']: [] for t in tickets}
|
|
in_degree = {t['id']: 0 for t in tickets}
|
|
|
|
for t in tickets:
|
|
for dep_id in t.get('depends_on', []):
|
|
if dep_id not in ticket_map:
|
|
raise ValueError(f"Missing dependency: Ticket '{t['id']}' depends on '{dep_id}', but '{dep_id}' is not in the ticket list.")
|
|
adj[dep_id].append(t['id'])
|
|
in_degree[t['id']] += 1
|
|
|
|
# 2. Find nodes with in-degree 0
|
|
queue = [t['id'] for t in tickets if in_degree[t['id']] == 0]
|
|
sorted_ids = []
|
|
|
|
# 3. Process queue
|
|
while queue:
|
|
u_id = queue.pop(0)
|
|
sorted_ids.append(u_id)
|
|
for v_id in adj[u_id]:
|
|
in_degree[v_id] -= 1
|
|
if in_degree[v_id] == 0:
|
|
queue.append(v_id)
|
|
|
|
# 4. Check for cycles
|
|
if len(sorted_ids) != len(tickets):
|
|
# Find which tickets are part of a cycle (or blocked by one)
|
|
remaining = [t_id for t_id in ticket_map if t_id not in sorted_ids]
|
|
raise ValueError(f"Circular dependency detected among tickets: {remaining}")
|
|
|
|
return [ticket_map[t_id] for t_id in sorted_ids]
|
|
|
|
if __name__ == "__main__":
|
|
# Quick test if run directly
|
|
test_brief = "Implement a new feature."
|
|
test_skeletons = "class NewFeature: pass"
|
|
tickets = generate_tickets(test_brief, test_skeletons)
|
|
print(json.dumps(tickets, indent=2))
|