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))