""" Recipe Service - business logic for recipe management. """ from typing import Optional, List, Dict, Any import json import yaml class RecipeService: """ Service for managing recipes. Handles recipe parsing, validation, and DAG building. """ def __init__(self, redis, cache): self.redis = redis self.cache = cache self.recipe_prefix = "recipe:" async def get_recipe(self, recipe_id: str) -> Optional[Dict[str, Any]]: """Get a recipe by ID (content hash).""" # First check Redis data = self.redis.get(f"{self.recipe_prefix}{recipe_id}") if data: return json.loads(data) # Fall back to cache path = self.cache.get_by_content_hash(recipe_id) if path and path.exists(): with open(path) as f: return yaml.safe_load(f) return None async def list_recipes(self, actor_id: str = None, page: int = 1, limit: int = 20) -> Dict[str, Any]: """List available recipes with pagination.""" recipes = [] cursor = 0 while True: cursor, keys = self.redis.scan( cursor=cursor, match=f"{self.recipe_prefix}*", count=100 ) for key in keys: data = self.redis.get(key) if data: recipe = json.loads(data) # Filter by actor if specified if actor_id is None or recipe.get("actor_id") == actor_id: recipes.append(recipe) if cursor == 0: break # Sort by name recipes.sort(key=lambda r: r.get("name", "")) # Paginate total = len(recipes) start = (page - 1) * limit end = start + limit page_recipes = recipes[start:end] return { "recipes": page_recipes, "pagination": { "page": page, "limit": limit, "total": total, "has_more": end < total, } } async def save_recipe(self, recipe_id: str, recipe_data: Dict[str, Any]) -> None: """Save a recipe to Redis.""" self.redis.set(f"{self.recipe_prefix}{recipe_id}", json.dumps(recipe_data)) async def delete_recipe(self, recipe_id: str) -> bool: """Delete a recipe.""" return self.redis.delete(f"{self.recipe_prefix}{recipe_id}") > 0 def parse_yaml(self, yaml_content: str) -> Dict[str, Any]: """Parse recipe YAML content.""" return yaml.safe_load(yaml_content) def build_dag(self, recipe: Dict[str, Any]) -> Dict[str, Any]: """ Build DAG visualization data from recipe. Returns nodes and edges for Cytoscape.js. """ nodes = [] edges = [] dag = recipe.get("dag", {}) dag_nodes = dag.get("nodes", {}) output_node = dag.get("output") for node_id, node_def in dag_nodes.items(): node_type = node_def.get("type", "EFFECT") nodes.append({ "data": { "id": node_id, "label": node_id, "nodeType": node_type, "isOutput": node_id == output_node, } }) # Build edges from inputs for input_ref in node_def.get("inputs", []): if isinstance(input_ref, dict): source = input_ref.get("node") or input_ref.get("input") else: source = input_ref if source: edges.append({ "data": { "source": source, "target": node_id, } }) return {"nodes": nodes, "edges": edges}