# Copied from template-manager (2)/template-manager/tech_stack_service.py # See original for full implementation details #!/usr/bin/env python3 """ Complete Tech Stack Recommendation Service Consolidated service that includes all essential functionality: - AI-powered tech stack recommendations - Claude API integration - Feature extraction - Neo4j knowledge graph operations - Database operations """ import os import sys import json import asyncio import asyncpg from datetime import datetime from typing import Dict, List, Any, Optional from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field import uvicorn from loguru import logger import anthropic import requests from neo4j import AsyncGraphDatabase # Configure logging logger.remove() # Check if running as command line tool if len(sys.argv) > 2 and sys.argv[1] == "--template-id": # For command line usage, output logs to stderr logger.add(lambda msg: print(msg, end="", file=sys.stderr), level="ERROR", format="{time} | {level} | {message}") else: # For server usage, output logs to stdout logger.add(lambda msg: print(msg, end=""), level="INFO", format="{time} | {level} | {message}") # ============================================================================ # PYDANTIC MODELS # ============================================================================ class TechRecommendationRequest(BaseModel): template_id: str = Field(..., description="Template ID to get recommendations for") class TechRecommendationResponse(BaseModel): template_id: str stack_name: str monthly_cost: float setup_cost: float team_size: str development_time: int satisfaction: int success_rate: int frontend: str backend: str database: str cloud: str testing: str mobile: str devops: str ai_ml: str # Single recommended tool recommended_tool: str = "" recommendation_score: float created_at: datetime # ============================================================================ # CLAUDE CLIENT # ============================================================================ class ClaudeClient: """Claude API client for tech stack recommendations""" def __init__(self): # Claude API configuration self.api_key = os.getenv("CLAUDE_API_KEY") if not self.api_key: logger.warning("CLAUDE_API_KEY environment variable not set - AI features will be limited") self.client = None else: # Initialize Anthropic client self.client = anthropic.Anthropic(api_key=self.api_key) # Database configuration with fallback self.db_config = self._get_db_config() logger.info("ClaudeClient initialized") def _get_db_config(self): """Get database configuration with fallback options""" # Try environment variables first host = os.getenv("POSTGRES_HOST") if not host: # Check if running inside Docker (postgres hostname available) try: import socket socket.gethostbyname("postgres") host = "postgres" # Docker internal network except socket.gaierror: # Not in Docker, use localhost host = "localhost" return { "host": host, "port": int(os.getenv("POSTGRES_PORT", "5432")), "database": os.getenv("POSTGRES_DB", "dev_pipeline"), "user": os.getenv("POSTGRES_USER", "pipeline_admin"), "password": os.getenv("POSTGRES_PASSWORD", "secure_pipeline_2024") } async def connect_db(self): """Create database connection""" try: conn = await asyncpg.connect(**self.db_config) logger.info("Database connected successfully") return conn except Exception as e: logger.error(f"Database connection failed: {e}") raise def create_prompt(self, template_data: Dict[str, Any], keywords: List[str]) -> str: """Create a prompt for Claude API""" prompt = f""" You are a tech stack recommendation expert. Based on the following template information and extracted keywords, recommend a complete tech stack solution including both technologies and ONE essential business tool. Template Information: - Type: {template_data.get('type', 'N/A')} - Title: {template_data.get('title', 'N/A')} - Description: {template_data.get('description', 'N/A')} - Category: {template_data.get('category', 'N/A')} Extracted Keywords: {', '.join(keywords) if keywords else 'None'} Please provide a complete tech stack recommendation in the following JSON format. Include realistic cost estimates, team size, development time, success metrics, and ONE relevant business tool. {{ "stack_name": "MVP Startup Stack", "monthly_cost": 65.0, "setup_cost": 850.0, "team_size": "2-4", "development_time": 3, "satisfaction": 85, "success_rate": 88, "frontend": "Next.js", "backend": "Node.js", "database": "PostgreSQL", "cloud": "Railway", "testing": "Jest", "mobile": "React Native", "devops": "GitHub Actions", "ai_ml": "Hugging Face", "recommended_tool": "Shopify", "recommendation_score": 96.5 }} Guidelines: - Choose technologies that work well together - Provide realistic cost estimates based on the template complexity - Estimate development time in months - Include satisfaction and success rate percentages (0-100) - Set recommendation_score based on how well the stack fits the requirements (0-100) - Use modern, popular technologies - Consider the template's business domain and technical requirements - Select ONLY ONE tool total that best complements the entire tech stack - Choose the most appropriate tool for the template's specific needs and industry - The tool should be the most essential business tool for this particular template IMPORTANT TOOL SELECTION RULES: - For E-commerce/Online Store templates: Use Shopify, WooCommerce, or Magento - For CRM/Customer Management: Use Salesforce, HubSpot, or Zoho CRM - For Analytics/Data: Use Google Analytics, Mixpanel, or Tableau - For Payment Processing: Use Stripe, PayPal, or Razorpay - For Communication/Collaboration: Use Slack, Microsoft Teams, or Discord - For Project Management: Use Trello, Jira, or Asana - For Marketing: Use Mailchimp, SendGrid, or Constant Contact - For Social Media: Use Hootsuite, Buffer, or Sprout Social - For AI/ML projects: Use TensorFlow, PyTorch, or Hugging Face - For Mobile Apps: Use Firebase, AWS Amplify, or App Store Connect - For Enterprise: Use Microsoft 365, Google Workspace, or Atlassian - For Startups: Use Notion, Airtable, or Zapier Choose the tool that BEST matches the template's primary business function and industry. Provide only the JSON response, no additional text. """ return prompt async def get_recommendation(self, template_id: str) -> Dict[str, Any]: """Get tech stack recommendation from Claude API""" try: if not self.client: raise HTTPException(status_code=503, detail="Claude API not available - API key not configured") conn = await self.connect_db() # Get template data - check both templates and custom_templates tables template_query = """ SELECT id, type, title, description, category FROM templates WHERE id = $1 """ template_result = await conn.fetchrow(template_query, template_id) if not template_result: # Try custom_templates table template_query = """ SELECT id, type, title, description, category FROM custom_templates WHERE id = $1 """ template_result = await conn.fetchrow(template_query, template_id) if not template_result: await conn.close() raise HTTPException(status_code=404, detail="Template not found") template_data = dict(template_result) # Get extracted keywords keywords_result = await conn.fetchrow(''' SELECT keywords_json FROM extracted_keywords WHERE template_id = $1 AND keywords_json IS NOT NULL ORDER BY created_at DESC LIMIT 1 ''', template_id) keywords = [] if keywords_result: keywords = json.loads(keywords_result['keywords_json']) await conn.close() # Create prompt with extracted keywords prompt = self.create_prompt(template_data, keywords) # Call Claude API response = self.client.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=2000, temperature=0.7, messages=[{"role": "user", "content": prompt}] ) # Parse response response_text = response.content[0].text.strip() # Extract JSON from response if response_text.startswith('```json'): response_text = response_text[7:-3] elif response_text.startswith('```'): response_text = response_text[3:-3] response_data = json.loads(response_text) # Store recommendation await self.store_tech_recommendations(template_id, response_data) # Auto-migrate new recommendation to Neo4j try: await self.auto_migrate_single_recommendation(template_id) except Exception as e: logger.warning(f"Auto-migration failed for template {template_id}: {e}") return response_data except Exception as e: logger.error(f"Error getting recommendation: {e}") raise HTTPException(status_code=500, detail=f"Failed to get recommendation: {str(e)}") async def store_tech_recommendations(self, template_id: str, response_data: Dict[str, Any]): """Store tech recommendations in tech_stack_recommendations table""" try: conn = await self.connect_db() # Clear existing recommendations for this template await conn.execute( "DELETE FROM tech_stack_recommendations WHERE template_id = $1", template_id ) # Handle fields that could be dict or string def format_field(field_value): if isinstance(field_value, dict): return json.dumps(field_value) return str(field_value) if field_value is not None else '' # Handle single tool def format_tool(tool_value): if isinstance(tool_value, str): return tool_value return '' # Store the complete tech stack in the proper table await conn.execute( """ INSERT INTO tech_stack_recommendations (template_id, stack_name, monthly_cost, setup_cost, team_size, development_time, satisfaction, success_rate, frontend, backend, database, cloud, testing, mobile, devops, ai_ml, recommended_tool, recommendation_score) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) """, template_id, response_data.get('stack_name', 'Tech Stack'), response_data.get('monthly_cost', 0.0), response_data.get('setup_cost', 0.0), response_data.get('team_size', '1-2'), response_data.get('development_time', 1), response_data.get('satisfaction', 0), response_data.get('success_rate', 0), format_field(response_data.get('frontend', '')), format_field(response_data.get('backend', '')), format_field(response_data.get('database', '')), format_field(response_data.get('cloud', '')), format_field(response_data.get('testing', '')), format_field(response_data.get('mobile', '')), format_field(response_data.get('devops', '')), format_field(response_data.get('ai_ml', '')), format_tool(response_data.get('recommended_tool', '')), response_data.get('recommendation_score', 0.0) ) await conn.close() logger.info(f"Stored complete tech stack with tools for template {template_id} in tech_stack_recommendations table") except Exception as e: logger.error(f"Error storing tech recommendations: {e}") async def auto_migrate_single_recommendation(self, template_id: str): """Auto-migrate a single recommendation from tech_stack_recommendations table to Neo4j""" try: logger.info(f"Starting auto-migration for template {template_id}") conn = await self.connect_db() # Get recommendation from tech_stack_recommendations table rec_query = """ SELECT * FROM tech_stack_recommendations WHERE template_id = $1 ORDER BY created_at DESC LIMIT 1 """ rec = await conn.fetchrow(rec_query, template_id) if not rec: logger.warning(f"No recommendation found in tech_stack_recommendations for template {template_id}") await conn.close() return logger.info(f"Found recommendation: {rec['stack_name']} for template {template_id}") # Get template data for context - check both templates and custom_templates tables template_query = """ SELECT id, title, description, category, type FROM templates WHERE id = $1 """ template_result = await conn.fetchrow(template_query, template_id) if not template_result: # Try custom_templates table template_query = """ SELECT id, title, description, category, type FROM custom_templates WHERE id = $1 """ template_result = await conn.fetchrow(template_query, template_id) if not template_result: logger.warning(f"Template {template_id} not found in templates or custom_templates tables") await conn.close() return template_data = dict(template_result) template_data['id'] = str(template_data['id']) # Get extracted keywords keywords_result = await conn.fetchrow(''' SELECT keywords_json FROM extracted_keywords WHERE template_id = $1 AND keywords_json IS NOT NULL ORDER BY created_at DESC LIMIT 1 ''', template_id) keywords = [] if keywords_result: keywords = json.loads(keywords_result['keywords_json']) await conn.close() # Create template node in Neo4j await neo4j_client.create_template_node(template_data) # Create tech stack node tech_stack_data = { "name": rec['stack_name'], "category": "tech_stack", "maturity_score": 0.9, "learning_curve": "medium", "performance_rating": float(rec['recommendation_score']) / 100.0 } await neo4j_client.create_technology_node(tech_stack_data) # Create recommendation relationship await neo4j_client.create_recommendation_relationship( str(template_id), rec['stack_name'], "tech_stack", float(rec['recommendation_score']) / 100.0 ) # Create individual technology nodes and relationships tech_fields = ['frontend', 'backend', 'database', 'cloud', 'testing', 'mobile', 'devops', 'ai_ml'] for field in tech_fields: tech_value = rec[field] if tech_value and tech_value.strip(): # Parse JSON if it's a string if isinstance(tech_value, str) and tech_value.startswith('{'): try: tech_value = json.loads(tech_value) if isinstance(tech_value, dict): tech_name = tech_value.get('name', str(tech_value)) else: tech_name = str(tech_value) except: tech_name = str(tech_value) else: tech_name = str(tech_value) # Create technology node tech_data = { "name": tech_name, "category": field, "maturity_score": 0.8, "learning_curve": "medium", "performance_rating": 0.8 } await neo4j_client.create_technology_node(tech_data) # Create relationship await neo4j_client.create_recommendation_relationship( str(template_id), tech_name, field, 0.8 ) # Create tool node for single recommended tool recommended_tool = rec.get('recommended_tool', '') if recommended_tool and recommended_tool.strip(): # Create tool node tool_data = { "name": recommended_tool, "category": "business_tool", "type": "Tool", "maturity_score": 0.8, "learning_curve": "easy", "performance_rating": 0.8 } await neo4j_client.create_technology_node(tool_data) # Create relationship await neo4j_client.create_recommendation_relationship( str(template_id), recommended_tool, "business_tool", 0.8 ) # Create keyword relationships if keywords and len(keywords) > 0: logger.info(f"Creating {len(keywords)} keyword relationships for template {template_id}") for keyword in keywords: if keyword and keyword.strip(): await neo4j_client.create_keyword_relationship(str(template_id), keyword) else: logger.warning(f"No keywords found for template {template_id}, skipping keyword relationships") # Create TemplateRecommendation node with rich data recommendation_data = { 'stack_name': rec['stack_name'], 'description': template_data.get('description', ''), 'project_scale': 'medium', 'team_size': 3, 'experience_level': 'intermediate', 'confidence_score': int(rec['recommendation_score']), 'recommendation_reasons': [ f"Tech stack: {rec['stack_name']}", f"Score: {rec['recommendation_score']}/100", "AI-generated recommendation" ], 'key_features': [ f"Frontend: {rec.get('frontend', 'N/A')}", f"Backend: {rec.get('backend', 'N/A')}", f"Database: {rec.get('database', 'N/A')}", f"Cloud: {rec.get('cloud', 'N/A')}" ], 'estimated_development_time_months': rec.get('development_time', 3), 'complexity_level': 'medium', 'budget_range_usd': f"${rec.get('monthly_cost', 0):.0f} - ${rec.get('setup_cost', 0):.0f}", 'time_to_market_weeks': rec.get('development_time', 3) * 4, 'scalability_requirements': 'moderate', 'security_requirements': 'standard', 'success_rate_percentage': rec.get('success_rate', 85), 'user_satisfaction_score': rec.get('satisfaction', 85) } await neo4j_client.create_template_recommendation_node(str(template_id), recommendation_data) # Create HAS_RECOMMENDATION relationship between Template and TemplateRecommendation await neo4j_client.create_has_recommendation_relationship(str(template_id), f"rec-{template_id}") logger.info(f"โœ… Successfully auto-migrated template {template_id} to Neo4j knowledge graph") except Exception as e: logger.error(f"Error in auto-migration for template {template_id}: {e}") # ============================================================================ # FEATURE EXTRACTOR # ============================================================================ class FeatureExtractor: """Extracts features from templates and gets tech stack recommendations""" def __init__(self): # Database configurations with fallback self.template_db_config = self._get_db_config() # Claude API configuration self.claude_api_key = os.getenv("CLAUDE_API_KEY") if not self.claude_api_key: logger.warning("CLAUDE_API_KEY not set - AI features will be limited") self.claude_client = anthropic.Anthropic(api_key=self.claude_api_key) if self.claude_api_key else None logger.info("FeatureExtractor initialized") def _get_db_config(self): """Get database configuration with fallback options""" # Try environment variables first host = os.getenv("POSTGRES_HOST") if not host: # Check if running inside Docker (postgres hostname available) try: import socket socket.gethostbyname("postgres") host = "postgres" # Docker internal network except socket.gaierror: # Not in Docker, use localhost host = "localhost" return { "host": host, "port": int(os.getenv("POSTGRES_PORT", "5432")), "database": os.getenv("POSTGRES_DB", "dev_pipeline"), "user": os.getenv("POSTGRES_USER", "pipeline_admin"), "password": os.getenv("POSTGRES_PASSWORD", "secure_pipeline_2024") } async def connect_db(self): """Create database connection""" try: conn = await asyncpg.connect(**self.template_db_config) logger.info("Database connected successfully") return conn except Exception as e: logger.error(f"Database connection failed: {e}") raise async def extract_keywords_from_template(self, template_data: Dict[str, Any]) -> List[str]: """Extract keywords from template using local NLP processing""" try: # Combine all text data text_content = f"{template_data.get('title', '')} {template_data.get('description', '')} {template_data.get('category', '')}" # Clean and process text keywords = self._extract_keywords_local(text_content) logger.info(f"Extracted {len(keywords)} keywords locally: {keywords}") return keywords except Exception as e: logger.error(f"Error extracting keywords: {e}") return [] def _extract_keywords_local(self, text: str) -> List[str]: """Extract keywords using local text processing""" import re from collections import Counter # Define technical and business keywords tech_keywords = { 'web', 'api', 'database', 'frontend', 'backend', 'mobile', 'cloud', 'ai', 'ml', 'analytics', 'ecommerce', 'e-commerce', 'payment', 'authentication', 'security', 'testing', 'deployment', 'microservices', 'rest', 'graphql', 'react', 'angular', 'vue', 'node', 'python', 'java', 'javascript', 'typescript', 'docker', 'kubernetes', 'aws', 'azure', 'gcp', 'postgresql', 'mysql', 'mongodb', 'redis', 'elasticsearch', 'rabbitmq', 'kafka', 'nginx', 'jenkins', 'gitlab', 'github', 'ci', 'cd', 'devops', 'monitoring', 'logging', 'caching', 'scaling' } business_keywords = { 'healthcare', 'medical', 'patient', 'appointment', 'records', 'telehealth', 'pharmacy', 'finance', 'banking', 'payment', 'invoice', 'accounting', 'trading', 'investment', 'education', 'learning', 'student', 'course', 'training', 'certification', 'lms', 'retail', 'inventory', 'shopping', 'cart', 'checkout', 'order', 'shipping', 'warehouse', 'crm', 'sales', 'marketing', 'lead', 'customer', 'support', 'ticket', 'workflow', 'automation', 'process', 'approval', 'document', 'file', 'content', 'management', 'enterprise', 'business', 'solution', 'platform', 'service', 'application', 'system' } # Clean text text = re.sub(r'[^\w\s-]', ' ', text.lower()) words = re.findall(r'\b\w+\b', text) # Filter out common stop words stop_words = { 'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'from', 'up', 'about', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'between', 'among', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'must', 'can', 'this', 'that', 'these', 'those', 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her', 'us', 'them' } # Filter words filtered_words = [word for word in words if len(word) > 2 and word not in stop_words] # Count word frequency word_counts = Counter(filtered_words) # Extract relevant keywords keywords = [] # Add technical keywords found in text for word in filtered_words: if word in tech_keywords or word in business_keywords: keywords.append(word) # Add most frequent meaningful words (excluding already added keywords) remaining_words = [word for word, count in word_counts.most_common(10) if word not in keywords and count > 1] keywords.extend(remaining_words[:5]) # Remove duplicates and limit to 10 keywords unique_keywords = list(dict.fromkeys(keywords))[:10] return unique_keywords async def get_template_data(self, template_id: str) -> Optional[Dict[str, Any]]: """Get template data from database""" try: conn = await self.connect_db() # Try templates table first template = await conn.fetchrow( """ SELECT id, title, description, category, type FROM templates WHERE id = $1 """, template_id ) if not template: # Try custom_templates table template = await conn.fetchrow( """ SELECT id, title, description, category, type FROM custom_templates WHERE id = $1 """, template_id ) await conn.close() if template: return dict(template) return None except Exception as e: logger.error(f"Error getting template data: {e}") return None async def get_all_templates(self) -> List[Dict[str, Any]]: """Get all templates from both tables""" try: conn = await self.connect_db() # Get from templates table templates = await conn.fetch( """ SELECT id, title, description, category, type FROM templates WHERE type NOT IN ('_system', '_migration', '_test') """ ) # Get from custom_templates table custom_templates = await conn.fetch( """ SELECT id, title, description, category, type FROM custom_templates """ ) await conn.close() # Combine results all_templates = [] for template in templates: all_templates.append(dict(template)) for template in custom_templates: all_templates.append(dict(template)) return all_templates except Exception as e: logger.error(f"Error getting all templates: {e}") return [] async def store_extracted_keywords(self, template_id: str, keywords: List[str]): """Store extracted keywords in database""" try: conn = await self.connect_db() # Determine template source template_source = 'templates' template = await conn.fetchrow("SELECT id FROM templates WHERE id = $1", template_id) if not template: template_source = 'custom_templates' # Store keywords await conn.execute( """ INSERT INTO extracted_keywords (template_id, template_source, keywords_json, created_at) VALUES ($1, $2, $3, $4) ON CONFLICT (template_id, template_source) DO UPDATE SET keywords_json = $3, updated_at = $4 """, template_id, template_source, json.dumps(keywords), datetime.now() ) await conn.close() logger.info(f"Stored keywords for template {template_id} from {template_source}") except Exception as e: logger.error(f"Error storing extracted keywords: {e}") async def store_keywords(self, template_id: str, keywords: List[str]): """Store extracted keywords in database""" try: conn = await self.connect_db() # Store keywords await conn.execute( """ INSERT INTO extracted_keywords (template_id, keywords_json, created_at) VALUES ($1, $2, $3) ON CONFLICT (template_id) DO UPDATE SET keywords_json = $2, updated_at = $3 """, template_id, json.dumps(keywords), datetime.now() ) await conn.close() logger.info(f"Stored keywords for template {template_id}") except Exception as e: logger.error(f"Error storing keywords: {e}") # ============================================================================ # NEO4J CLIENT # ============================================================================ class Neo4jClient: """Neo4j client for knowledge graph operations""" def __init__(self): # Neo4j configuration - try multiple connection options self.uri = self._get_neo4j_uri() self.username = os.getenv("NEO4J_USERNAME", "neo4j") self.password = os.getenv("NEO4J_PASSWORD", "password") # Create driver self._create_driver() def _get_neo4j_uri(self): """Get Neo4j URI with fallback options""" # Try environment variable first uri = os.getenv("NEO4J_URI") if uri: return uri # Check if running inside Docker (neo4j hostname available) try: import socket socket.gethostbyname("neo4j") return "bolt://neo4j:7687" # Docker internal network except socket.gaierror: # Not in Docker, use localhost return "bolt://localhost:7687" def _create_driver(self): """Create Neo4j driver""" self.driver = AsyncGraphDatabase.driver( self.uri, auth=(self.username, self.password) ) logger.info(f"Neo4jClient initialized with URI: {self.uri}") async def close(self): """Close the Neo4j driver""" await self.driver.close() logger.info("Neo4j connection closed") async def test_connection(self): """Test Neo4j connection""" try: async with self.driver.session() as session: result = await session.run("RETURN 1 as test") record = await result.single() if record and record["test"] == 1: logger.info("Neo4j connection successful") return True else: logger.error("Neo4j connection test failed") return False except Exception as e: logger.error(f"Neo4j connection failed: {e}") return False async def create_constraints(self): """Create Neo4j constraints""" try: async with self.driver.session() as session: # Create constraints constraints = [ "CREATE CONSTRAINT template_id_unique IF NOT EXISTS FOR (t:Template) REQUIRE t.id IS UNIQUE", "CREATE CONSTRAINT technology_name_unique IF NOT EXISTS FOR (tech:Technology) REQUIRE tech.name IS UNIQUE", "CREATE CONSTRAINT keyword_name_unique IF NOT EXISTS FOR (k:Keyword) REQUIRE k.name IS UNIQUE" ] for constraint in constraints: try: await session.run(constraint) except Exception as e: logger.warning(f"Constraint creation warning: {e}") logger.info("Neo4j constraints created successfully") except Exception as e: logger.error(f"Error creating constraints: {e}") async def create_template_node(self, template_data: Dict[str, Any]): """Create or update template node""" try: async with self.driver.session() as session: await session.run( """ MERGE (t:Template {id: $id}) SET t.name = $name, t.description = $description, t.category = $category, t.type = $type, t.updated_at = datetime() """, id=template_data.get('id'), name=template_data.get('name', template_data.get('title', '')), description=template_data.get('description', ''), category=template_data.get('category', ''), type=template_data.get('type', '') ) logger.info(f"Created/updated template node: {template_data.get('name', template_data.get('title', ''))}") except Exception as e: logger.error(f"Error creating template node: {e}") async def create_technology_node(self, tech_data: Dict[str, Any]): """Create or update technology node""" try: async with self.driver.session() as session: await session.run( """ MERGE (tech:Technology {name: $name}) SET tech.category = $category, tech.type = $type, tech.maturity_score = $maturity_score, tech.learning_curve = $learning_curve, tech.performance_rating = $performance_rating, tech.updated_at = datetime() """, name=tech_data.get('name'), category=tech_data.get('category', ''), type=tech_data.get('type', 'Technology'), maturity_score=tech_data.get('maturity_score', 0.8), learning_curve=tech_data.get('learning_curve', 'medium'), performance_rating=tech_data.get('performance_rating', 0.8) ) logger.info(f"Created/updated technology node: {tech_data.get('name')}") except Exception as e: logger.error(f"Error creating technology node: {e}") async def create_recommendation_relationship(self, template_id: str, tech_name: str, category: str, score: float): """Create recommendation relationship""" try: async with self.driver.session() as session: await session.run( """ MATCH (t:Template {id: $template_id}) MATCH (tech:Technology {name: $tech_name}) MERGE (t)-[r:RECOMMENDED_TECHNOLOGY {category: $category, score: $score}]->(tech) SET r.updated_at = datetime() """, template_id=template_id, tech_name=tech_name, category=category, score=score ) logger.info(f"Created recommendation relationship: {template_id} -> {tech_name}") except Exception as e: logger.error(f"Error creating recommendation relationship: {e}") async def create_keyword_relationship(self, template_id: str, keyword: str): """Create keyword relationship""" try: async with self.driver.session() as session: # Create keyword node await session.run( """ MERGE (k:Keyword {name: $keyword}) SET k.updated_at = datetime() """, keyword=keyword ) # Create relationship await session.run( """ MATCH (t:Template {id: $template_id}) MATCH (k:Keyword {name: $keyword}) MERGE (t)-[r:HAS_KEYWORD]->(k) SET r.updated_at = datetime() """, template_id=template_id, keyword=keyword ) logger.info(f"Created keyword relationship: {template_id} -> {keyword}") except Exception as e: logger.error(f"Error creating keyword relationship: {e}") async def create_has_recommendation_relationship(self, template_id: str, recommendation_id: str): """Create HAS_RECOMMENDATION relationship between Template and TemplateRecommendation""" try: async with self.driver.session() as session: await session.run( """ MATCH (t:Template {id: $template_id}) MATCH (tr:TemplateRecommendation {id: $recommendation_id}) MERGE (t)-[r:HAS_RECOMMENDATION]->(tr) SET r.created_at = datetime(), r.updated_at = datetime() """, template_id=template_id, recommendation_id=recommendation_id ) logger.info(f"Created HAS_RECOMMENDATION relationship: {template_id} -> {recommendation_id}") except Exception as e: logger.error(f"Error creating HAS_RECOMMENDATION relationship: {e}") async def get_recommendations_from_neo4j(self, template_id: str) -> Optional[Dict[str, Any]]: """Get tech stack recommendations from Neo4j knowledge graph""" try: # Convert UUID to string if needed template_id_str = str(template_id) async with self.driver.session() as session: # Query for template recommendations from Neo4j result = await session.run( """ MATCH (t:Template {id: $template_id})-[:HAS_RECOMMENDATION]->(tr:TemplateRecommendation) OPTIONAL MATCH (t)-[r:RECOMMENDED_TECHNOLOGY]->(tech:Technology) WITH tr, collect({ name: tech.name, category: r.category, score: r.score, type: tech.type, maturity_score: tech.maturity_score, learning_curve: tech.learning_curve, performance_rating: tech.performance_rating }) as technologies RETURN tr.business_domain as business_domain, tr.project_type as project_type, tr.team_size as team_size, tr.confidence_score as confidence_score, tr.estimated_development_time_months as development_time, tr.success_rate_percentage as success_rate, tr.user_satisfaction_score as satisfaction, tr.budget_range_usd as budget_range, tr.complexity_level as complexity_level, technologies ORDER BY tr.created_at DESC LIMIT 1 """, template_id=template_id_str ) record = await result.single() if record: # Process technologies by category tech_categories = {} for tech in record['technologies']: category = tech['category'] if category not in tech_categories: tech_categories[category] = [] tech_categories[category].append(tech) # Build recommendation response recommendation = { 'stack_name': f"{record['business_domain']} {record['project_type']} Stack", 'monthly_cost': record['budget_range'] / 12 if record['budget_range'] else 1000, 'setup_cost': record['budget_range'] if record['budget_range'] else 5000, 'team_size': record['team_size'] or '2-4', 'development_time': record['development_time'] or 6, 'satisfaction': record['satisfaction'] or 85, 'success_rate': record['success_rate'] or 80, 'frontend': '', 'backend': '', 'database': '', 'cloud': '', 'testing': '', 'mobile': '', 'devops': '', 'ai_ml': '', 'recommended_tool': '', 'recommendation_score': record['confidence_score'] or 85.0 } # Map technologies to categories for category, techs in tech_categories.items(): if techs: best_tech = max(techs, key=lambda x: x['score']) if category.lower() == 'frontend': recommendation['frontend'] = best_tech['name'] elif category.lower() == 'backend': recommendation['backend'] = best_tech['name'] elif category.lower() == 'database': recommendation['database'] = best_tech['name'] elif category.lower() == 'cloud': recommendation['cloud'] = best_tech['name'] elif category.lower() == 'testing': recommendation['testing'] = best_tech['name'] elif category.lower() == 'mobile': recommendation['mobile'] = best_tech['name'] elif category.lower() == 'devops': recommendation['devops'] = best_tech['name'] elif category.lower() in ['ai', 'ml', 'ai_ml']: recommendation['ai_ml'] = best_tech['name'] elif category.lower() == 'tool': recommendation['recommended_tool'] = best_tech['name'] logger.info(f"Found recommendations in Neo4j for template {template_id}: {recommendation['stack_name']}") return recommendation else: logger.info(f"No recommendations found in Neo4j for template {template_id}") return None except Exception as e: logger.error(f"Error getting recommendations from Neo4j: {e}") return None async def create_template_recommendation_node(self, template_id: str, recommendation_data: Dict[str, Any]): """Create TemplateRecommendation node with rich data""" try: async with self.driver.session() as session: # Extract business domain from template category or description business_domain = self._extract_business_domain(recommendation_data) project_type = self._extract_project_type(recommendation_data) # Create TemplateRecommendation node await session.run( """ MERGE (tr:TemplateRecommendation {id: $id}) SET tr.business_domain = $business_domain, tr.project_type = $project_type, tr.project_scale = $project_scale, tr.team_size = $team_size, tr.experience_level = $experience_level, tr.confidence_score = $confidence_score, tr.recommendation_reasons = $recommendation_reasons, tr.key_features = $key_features, tr.estimated_development_time_months = $estimated_development_time_months, tr.complexity_level = $complexity_level, tr.budget_range_usd = $budget_range_usd, tr.time_to_market_weeks = $time_to_market_weeks, tr.scalability_requirements = $scalability_requirements, tr.security_requirements = $security_requirements, tr.success_rate_percentage = $success_rate_percentage, tr.user_satisfaction_score = $user_satisfaction_score, tr.created_by_system = $created_by_system, tr.recommendation_source = $recommendation_source, tr.is_active = $is_active, tr.usage_count = $usage_count, tr.created_at = datetime(), tr.updated_at = datetime() """, id=f"rec-{template_id}", business_domain=business_domain, project_type=project_type, project_scale=recommendation_data.get('project_scale', 'medium'), team_size=recommendation_data.get('team_size', 3), experience_level=recommendation_data.get('experience_level', 'intermediate'), confidence_score=recommendation_data.get('confidence_score', 85), recommendation_reasons=recommendation_data.get('recommendation_reasons', ['AI-generated recommendation']), key_features=recommendation_data.get('key_features', []), estimated_development_time_months=recommendation_data.get('estimated_development_time_months', 3), complexity_level=recommendation_data.get('complexity_level', 'medium'), budget_range_usd=recommendation_data.get('budget_range_usd', '$5,000 - $15,000'), time_to_market_weeks=recommendation_data.get('time_to_market_weeks', 12), scalability_requirements=recommendation_data.get('scalability_requirements', 'moderate'), security_requirements=recommendation_data.get('security_requirements', 'standard'), success_rate_percentage=recommendation_data.get('success_rate_percentage', 85), user_satisfaction_score=recommendation_data.get('user_satisfaction_score', 85), created_by_system=True, recommendation_source='ai_model', is_active=True, usage_count=0 ) # Create relationship from Template to TemplateRecommendation await session.run( """ MATCH (t:Template {id: $template_id}) MATCH (tr:TemplateRecommendation {id: $rec_id}) MERGE (t)-[:RECOMMENDED_FOR]->(tr) """, template_id=template_id, rec_id=f"rec-{template_id}" ) logger.info(f"Created TemplateRecommendation node: rec-{template_id}") except Exception as e: logger.error(f"Error creating TemplateRecommendation node: {e}") def _extract_business_domain(self, recommendation_data: Dict[str, Any]) -> str: """Extract business domain from recommendation data""" # Try to extract from stack name or description stack_name = recommendation_data.get('stack_name', '').lower() description = recommendation_data.get('description', '').lower() if any(word in stack_name or word in description for word in ['ecommerce', 'e-commerce', 'shop', 'store', 'retail']): return 'E-commerce' elif any(word in stack_name or word in description for word in ['social', 'community', 'network']): return 'Social Media' elif any(word in stack_name or word in description for word in ['finance', 'payment', 'banking', 'fintech']): return 'Fintech' elif any(word in stack_name or word in description for word in ['health', 'medical', 'care']): return 'Healthcare' elif any(word in stack_name or word in description for word in ['education', 'learning', 'course']): return 'Education' else: return 'General Business' def _extract_project_type(self, recommendation_data: Dict[str, Any]) -> str: """Extract project type from recommendation data""" stack_name = recommendation_data.get('stack_name', '').lower() description = recommendation_data.get('description', '').lower() if any(word in stack_name or word in description for word in ['web', 'website', 'portal']): return 'Web Application' elif any(word in stack_name or word in description for word in ['mobile', 'app', 'ios', 'android']): return 'Mobile Application' elif any(word in stack_name or word in description for word in ['api', 'service', 'microservice']): return 'API Service' elif any(word in stack_name or word in description for word in ['dashboard', 'admin', 'management']): return 'Management Dashboard' else: return 'Web Application' # ============================================================================ # FASTAPI APPLICATION # ============================================================================ # Initialize FastAPI app app = FastAPI( title="Tech Stack Recommendation Service", description="AI-powered tech stack recommendations with tools integration", version="1.0.0" ) # Add CORS middleware app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Initialize clients claude_client = ClaudeClient() feature_extractor = FeatureExtractor() neo4j_client = Neo4jClient() @app.on_event("startup") async def startup_event(): """Initialize services on startup""" print("๐Ÿš€ STARTING TECH STACK RECOMMENDATION SERVICE") print("=" * 50) print("โœ… AI Service will be available at: http://localhost:8013") print("โœ… API Documentation: http://localhost:8013/docs") print("โœ… Test endpoint: POST http://localhost:8013/ai/recommendations") print("=" * 50) # Automatic migration on startup print("๐Ÿ”„ Starting automatic migration to Neo4j...") try: await migrate_to_neo4j() print("โœ… Automatic migration completed successfully!") except Exception as e: print(f"โš ๏ธ Migration warning: {e}") print("โœ… Service will continue running with existing data") print("=" * 50) @app.get("/") async def root(): """Root endpoint""" return { "message": "Tech Stack Recommendation Service", "version": "1.0.0", "status": "running", "endpoints": { "recommendations": "POST /ai/recommendations", "docs": "GET /docs" } } @app.get("/health") async def health_check(): """Health check endpoint""" return {"status": "healthy", "timestamp": datetime.now()} @app.post("/ai/recommendations/formatted") async def get_formatted_tech_recommendations(request: TechRecommendationRequest): """Get tech stack recommendations in a formatted, user-friendly way""" try: logger.info(f"Getting formatted recommendations for template: {request.template_id}") # Get the standard recommendation conn = await claude_client.connect_db() recommendations = await conn.fetch(''' SELECT template_id, stack_name, monthly_cost, setup_cost, team_size, development_time, satisfaction, success_rate, frontend, backend, database, cloud, testing, mobile, devops, ai_ml, recommended_tool, recommendation_score, created_at, updated_at FROM tech_stack_recommendations WHERE template_id = $1 ORDER BY created_at DESC LIMIT 1 ''', request.template_id) if recommendations: rec = dict(recommendations[0]) await conn.close() # Format the response in a user-friendly way formatted_response = { "template_id": request.template_id, "tech_stack": { "name": rec.get('stack_name', 'Tech Stack'), "score": f"{rec.get('recommendation_score', 0.0)}/100", "technologies": { "Frontend": rec.get('frontend', ''), "Backend": rec.get('backend', ''), "Database": rec.get('database', ''), "Cloud": rec.get('cloud', ''), "Testing": rec.get('testing', ''), "Mobile": rec.get('mobile', ''), "DevOps": rec.get('devops', ''), "AI/ML": rec.get('ai_ml', '') }, "recommended_tool": rec.get('recommended_tool', ''), "costs": { "monthly": f"${rec.get('monthly_cost', 0.0)}", "setup": f"${rec.get('setup_cost', 0.0)}" }, "team": { "size": rec.get('team_size', '1-2'), "development_time": f"{rec.get('development_time', 1)} months" }, "metrics": { "satisfaction": f"{rec.get('satisfaction', 0)}%", "success_rate": f"{rec.get('success_rate', 0)}%" } }, "created_at": rec.get('created_at', datetime.now()) } return formatted_response else: await conn.close() return {"error": "No recommendations found for this template"} except Exception as e: logger.error(f"Error getting formatted recommendations: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.get("/extract-keywords/{template_id}") async def get_extracted_keywords(template_id: str): """Get extracted keywords for a specific template""" try: logger.info(f"Getting keywords for template: {template_id}") conn = await feature_extractor.connect_db() # Get keywords from database keywords_result = await conn.fetchrow(''' SELECT keywords_json, created_at, template_source FROM extracted_keywords WHERE template_id = $1 AND keywords_json IS NOT NULL ORDER BY created_at DESC LIMIT 1 ''', template_id) await conn.close() if not keywords_result: raise HTTPException(status_code=404, detail="No keywords found for this template") keywords = json.loads(keywords_result['keywords_json']) if keywords_result['keywords_json'] else [] return { "template_id": template_id, "keywords": keywords, "count": len(keywords), "created_at": keywords_result['created_at'], "template_source": keywords_result['template_source'] } except Exception as e: logger.error(f"Error getting keywords: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/extract-keywords/{template_id}") async def extract_keywords_for_template(template_id: str): """Extract keywords for a specific template""" try: logger.info(f"Extracting keywords for template: {template_id}") # Get template data from database template_data = await feature_extractor.get_template_data(template_id) if not template_data: raise HTTPException(status_code=404, detail="Template not found") # Extract keywords using local NLP keywords = await feature_extractor.extract_keywords_from_template(template_data) # Store keywords in database await feature_extractor.store_extracted_keywords(template_id, keywords) return { "template_id": template_id, "keywords": keywords, "count": len(keywords) } except Exception as e: logger.error(f"Error extracting keywords: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/extract-keywords-all") async def extract_keywords_for_all_templates(): """Extract keywords for all templates""" try: logger.info("Extracting keywords for all templates") # Get all templates from database templates = await feature_extractor.get_all_templates() results = [] for template in templates: try: # Extract keywords using Claude AI keywords = await feature_extractor.extract_keywords_from_template(template) # Store keywords in database await feature_extractor.store_extracted_keywords(template['id'], keywords) results.append({ "template_id": template['id'], "title": template['title'], "keywords": keywords, "count": len(keywords) }) except Exception as e: logger.error(f"Error extracting keywords for template {template['id']}: {e}") results.append({ "template_id": template['id'], "title": template['title'], "error": str(e) }) return { "total_templates": len(templates), "processed": len(results), "results": results } except Exception as e: logger.error(f"Error in bulk keyword extraction: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/auto-workflow/{template_id}") async def trigger_automatic_workflow(template_id: str): """Trigger complete automatic workflow for a new template""" try: logger.info(f"๐Ÿš€ Starting automatic workflow for template: {template_id}") # Step 1: Extract keywords logger.info("๐Ÿ“ Step 1: Extracting keywords...") template_data = await feature_extractor.get_template_data(template_id) if not template_data: raise HTTPException(status_code=404, detail="Template not found") keywords = await feature_extractor.extract_keywords_from_template(template_data) await feature_extractor.store_extracted_keywords(template_id, keywords) logger.info(f"โœ… Keywords extracted and stored: {len(keywords)} keywords") # Step 2: Generate tech stack recommendation logger.info("๐Ÿค– Step 2: Generating tech stack recommendation...") try: recommendation_data = await claude_client.get_recommendation(template_id) logger.info(f"โœ… Tech stack recommendation generated: {recommendation_data.get('stack_name', 'Unknown')}") except Exception as e: logger.warning(f"โš ๏ธ Claude AI failed (likely billing issue): {e}") logger.info("๐Ÿ”„ Using database fallback for recommendation...") # Check if recommendation already exists in database conn = await claude_client.connect_db() existing_rec = await conn.fetchrow(''' SELECT * FROM tech_stack_recommendations WHERE template_id = $1 ORDER BY created_at DESC LIMIT 1 ''', template_id) if existing_rec: recommendation_data = dict(existing_rec) logger.info(f"โœ… Found existing recommendation: {recommendation_data.get('stack_name', 'Unknown')}") else: # Create a basic recommendation recommendation_data = { 'stack_name': f'{template_data.get("title", "Template")} Tech Stack', 'monthly_cost': 100.0, 'setup_cost': 2000.0, 'team_size': '3-5', 'development_time': 6, 'satisfaction': 85, 'success_rate': 90, 'frontend': 'React.js', 'backend': 'Node.js', 'database': 'PostgreSQL', 'cloud': 'AWS', 'testing': 'Jest', 'mobile': 'React Native', 'devops': 'Docker', 'ai_ml': 'TensorFlow', 'recommended_tool': 'Custom Tool', 'recommendation_score': 85.0 } logger.info(f"โœ… Created basic recommendation: {recommendation_data.get('stack_name', 'Unknown')}") await conn.close() # Step 3: Auto-migrate to Neo4j logger.info("๐Ÿ”„ Step 3: Auto-migrating to Neo4j knowledge graph...") await claude_client.auto_migrate_single_recommendation(template_id) logger.info("โœ… Auto-migration to Neo4j completed") return { "template_id": template_id, "workflow_status": "completed", "steps_completed": [ "keyword_extraction", "tech_stack_recommendation", "neo4j_migration" ], "keywords_count": len(keywords), "stack_name": recommendation_data.get('stack_name', 'Unknown'), "message": "Complete workflow executed successfully" } except Exception as e: logger.error(f"Error in automatic workflow for template {template_id}: {e}") raise HTTPException(status_code=500, detail=f"Workflow failed: {str(e)}") @app.post("/auto-workflow-batch") async def trigger_automatic_workflow_batch(): """Trigger automatic workflow for all templates without recommendations""" try: logger.info("๐Ÿš€ Starting batch automatic workflow for all templates") # Get all templates without recommendations conn = await claude_client.connect_db() templates_query = """ SELECT t.id, t.title, t.description, t.category, t.type FROM templates t LEFT JOIN tech_stack_recommendations tsr ON t.id = tsr.template_id WHERE tsr.template_id IS NULL AND t.type NOT LIKE '_%' UNION SELECT ct.id, ct.title, ct.description, ct.category, ct.type FROM custom_templates ct LEFT JOIN tech_stack_recommendations tsr ON ct.id = tsr.template_id WHERE tsr.template_id IS NULL AND ct.type NOT LIKE '_%' """ templates = await conn.fetch(templates_query) await conn.close() logger.info(f"๐Ÿ“‹ Found {len(templates)} templates without recommendations") results = [] for i, template in enumerate(templates, 1): try: logger.info(f"๐Ÿ”„ Processing {i}/{len(templates)}: {template['title']}") # Trigger workflow for this template workflow_result = await trigger_automatic_workflow(template['id']) results.append({ "template_id": template['id'], "title": template['title'], "status": "success", "workflow_result": workflow_result }) except Exception as e: logger.error(f"Error processing template {template['id']}: {e}") results.append({ "template_id": template['id'], "title": template['title'], "status": "failed", "error": str(e) }) success_count = len([r for r in results if r['status'] == 'success']) failed_count = len([r for r in results if r['status'] == 'failed']) return { "message": f"Batch workflow completed: {success_count} success, {failed_count} failed", "total_templates": len(templates), "success_count": success_count, "failed_count": failed_count, "results": results } except Exception as e: logger.error(f"Error in batch automatic workflow: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/ai/recommendations") async def get_tech_recommendations(request: TechRecommendationRequest): """Get tech stack recommendations for a template""" try: logger.info(f"Getting recommendations for template: {request.template_id}") # 1. FIRST: Check Neo4j knowledge graph for recommendations logger.info("๐Ÿ” Checking Neo4j knowledge graph for recommendations...") neo4j_recommendation = await neo4j_client.get_recommendations_from_neo4j(request.template_id) if neo4j_recommendation: logger.info(f"โœ… Found recommendations in Neo4j: {neo4j_recommendation['stack_name']}") # Format the response from Neo4j data response = TechRecommendationResponse( template_id=request.template_id, stack_name=neo4j_recommendation.get('stack_name', 'Tech Stack'), monthly_cost=float(neo4j_recommendation.get('monthly_cost', 0.0)), setup_cost=float(neo4j_recommendation.get('setup_cost', 0.0)), team_size=neo4j_recommendation.get('team_size', '1-2'), development_time=neo4j_recommendation.get('development_time', 1), satisfaction=neo4j_recommendation.get('satisfaction', 0), success_rate=neo4j_recommendation.get('success_rate', 0), frontend=neo4j_recommendation.get('frontend', ''), backend=neo4j_recommendation.get('backend', ''), database=neo4j_recommendation.get('database', ''), cloud=neo4j_recommendation.get('cloud', ''), testing=neo4j_recommendation.get('testing', ''), mobile=neo4j_recommendation.get('mobile', ''), devops=neo4j_recommendation.get('devops', ''), ai_ml=neo4j_recommendation.get('ai_ml', ''), recommended_tool=neo4j_recommendation.get('recommended_tool', ''), recommendation_score=float(neo4j_recommendation.get('recommendation_score', 0.0)), created_at=datetime.now() ) # Log the complete tech stack with tool for visibility logger.info(f"๐Ÿ“‹ Complete Tech Stack Recommendation:") logger.info(f" ๐ŸŽฏ Stack: {response.stack_name}") logger.info(f" ๐Ÿ’ป Frontend: {response.frontend}") logger.info(f" โš™๏ธ Backend: {response.backend}") logger.info(f" ๐Ÿ—„๏ธ Database: {response.database}") logger.info(f" โ˜๏ธ Cloud: {response.cloud}") logger.info(f" ๐Ÿงช Testing: {response.testing}") logger.info(f" ๐Ÿ“ฑ Mobile: {response.mobile}") logger.info(f" ๐Ÿš€ DevOps: {response.devops}") logger.info(f" ๐Ÿค– AI/ML: {response.ai_ml}") logger.info(f" ๐Ÿ”ง Recommended Tool: {response.recommended_tool}") logger.info(f" โญ Score: {response.recommendation_score}") # Return in the requested format with recommendations array return { "recommendations": [ { "template_id": response.template_id, "stack_name": response.stack_name, "monthly_cost": response.monthly_cost, "setup_cost": response.setup_cost, "team_size": response.team_size, "development_time": response.development_time, "satisfaction": response.satisfaction, "success_rate": response.success_rate, "frontend": response.frontend, "backend": response.backend, "database": response.database, "cloud": response.cloud, "testing": response.testing, "mobile": response.mobile, "devops": response.devops, "ai_ml": response.ai_ml, "recommendation_score": response.recommendation_score } ] } else: # 2. SECOND: Check database as fallback logger.info("๐Ÿ” Neo4j not found, checking database as fallback...") conn = await claude_client.connect_db() recommendations = await conn.fetch(''' SELECT template_id, stack_name, monthly_cost, setup_cost, team_size, development_time, satisfaction, success_rate, frontend, backend, database, cloud, testing, mobile, devops, ai_ml, recommended_tool, recommendation_score, created_at, updated_at FROM tech_stack_recommendations WHERE template_id = $1 ORDER BY created_at DESC LIMIT 1 ''', request.template_id) if recommendations: rec = dict(recommendations[0]) logger.info(f"โœ… Found recommendations in database: {rec.get('stack_name', 'Unknown')}") # Auto-migrate to Neo4j when found in database try: logger.info("๐Ÿ”„ Auto-migrating database recommendation to Neo4j...") await claude_client.auto_migrate_single_recommendation(request.template_id) except Exception as e: logger.warning(f"Auto-migration failed for template {request.template_id}: {e}") await conn.close() # Format the response from database response = TechRecommendationResponse( template_id=request.template_id, stack_name=rec.get('stack_name', 'Tech Stack'), monthly_cost=float(rec.get('monthly_cost', 0.0)), setup_cost=float(rec.get('setup_cost', 0.0)), team_size=rec.get('team_size', '1-2'), development_time=rec.get('development_time', 1), satisfaction=rec.get('satisfaction', 0), success_rate=rec.get('success_rate', 0), frontend=rec.get('frontend', ''), backend=rec.get('backend', ''), database=rec.get('database', ''), cloud=rec.get('cloud', ''), testing=rec.get('testing', ''), mobile=rec.get('mobile', ''), devops=rec.get('devops', ''), ai_ml=rec.get('ai_ml', ''), recommended_tool=rec.get('recommended_tool', ''), recommendation_score=float(rec.get('recommendation_score', 0.0)), created_at=datetime.now() ) # Log the complete tech stack with tool for visibility logger.info(f"๐Ÿ“‹ Complete Tech Stack Recommendation (from database):") logger.info(f" ๐ŸŽฏ Stack: {response.stack_name}") logger.info(f" ๐Ÿ’ป Frontend: {response.frontend}") logger.info(f" โš™๏ธ Backend: {response.backend}") logger.info(f" ๐Ÿ—„๏ธ Database: {response.database}") logger.info(f" โ˜๏ธ Cloud: {response.cloud}") logger.info(f" ๐Ÿงช Testing: {response.testing}") logger.info(f" ๐Ÿ“ฑ Mobile: {response.mobile}") logger.info(f" ๐Ÿš€ DevOps: {response.devops}") logger.info(f" ๐Ÿค– AI/ML: {response.ai_ml}") logger.info(f" ๐Ÿ”ง Recommended Tool: {response.recommended_tool}") logger.info(f" โญ Score: {response.recommendation_score}") # Return in the requested format with recommendations array return { "recommendations": [ { "template_id": response.template_id, "stack_name": response.stack_name, "monthly_cost": response.monthly_cost, "setup_cost": response.setup_cost, "team_size": response.team_size, "development_time": response.development_time, "satisfaction": response.satisfaction, "success_rate": response.success_rate, "frontend": response.frontend, "backend": response.backend, "database": response.database, "cloud": response.cloud, "testing": response.testing, "mobile": response.mobile, "devops": response.devops, "ai_ml": response.ai_ml, "recommendation_score": response.recommendation_score } ] } else: # 3. THIRD: Generate new recommendations using Claude AI logger.info("๐Ÿ” No existing recommendations found, generating new ones with Claude AI...") await conn.close() response_data = await claude_client.get_recommendation(request.template_id) # Get keywords conn = await claude_client.connect_db() keywords_result = await conn.fetchrow(''' SELECT keywords_json FROM extracted_keywords WHERE template_id = $1 AND keywords_json IS NOT NULL ORDER BY template_source LIMIT 1 ''', request.template_id) keywords = [] if keywords_result: keywords = json.loads(keywords_result['keywords_json']) await conn.close() # Return in the requested format with recommendations array return { "recommendations": [ { "template_id": request.template_id, "stack_name": response_data.get('stack_name', 'Tech Stack'), "monthly_cost": float(response_data.get('monthly_cost', 0.0)), "setup_cost": float(response_data.get('setup_cost', 0.0)), "team_size": response_data.get('team_size', '1-2'), "development_time": response_data.get('development_time', 1), "satisfaction": response_data.get('satisfaction', 0), "success_rate": response_data.get('success_rate', 0), "frontend": response_data.get('frontend', ''), "backend": response_data.get('backend', ''), "database": response_data.get('database', ''), "cloud": response_data.get('cloud', ''), "testing": response_data.get('testing', ''), "mobile": response_data.get('mobile', ''), "devops": response_data.get('devops', ''), "ai_ml": response_data.get('ai_ml', ''), "recommendation_score": float(response_data.get('recommendation_score', 0.0)) } ] } except Exception as e: logger.error(f"Error getting recommendations: {e}") raise HTTPException(status_code=500, detail=str(e)) # ============================================================================ # MIGRATION FUNCTIONALITY # ============================================================================ async def migrate_to_neo4j(): """Migrate tech stack recommendations to Neo4j knowledge graph""" print("๐Ÿš€ Migrating Tech Stack Recommendations to Neo4j Knowledge Graph") print("=" * 70) try: # Test Neo4j connection if not await neo4j_client.test_connection(): print("โŒ Neo4j connection failed") return # Create constraints await neo4j_client.create_constraints() print("โœ… Neo4j constraints created") # Connect to PostgreSQL conn = await claude_client.connect_db() print("โœ… PostgreSQL connected") # Get templates with recommendations templates_query = """ SELECT DISTINCT t.id, t.title, t.description, t.category, t.type, t.created_at FROM templates t JOIN tech_stack_recommendations tsr ON t.id = tsr.template_id ORDER BY t.created_at DESC """ templates = await conn.fetch(templates_query) print(f"๐Ÿ“‹ Found {len(templates)} templates to migrate") for i, template in enumerate(templates, 1): print(f"\n๐Ÿ“ Processing {i}/{len(templates)}: {template['title']}") # Get recommendation rec_query = """ SELECT * FROM tech_stack_recommendations WHERE template_id = $1 ORDER BY created_at DESC LIMIT 1 """ rec = await conn.fetchrow(rec_query, template['id']) if not rec: print(" โš ๏ธ No recommendations found for this template") continue print(f" ๐Ÿ” Found recommendation: {rec['stack_name']}") # Get keywords for this template keywords_query = """ SELECT keywords_json FROM extracted_keywords WHERE template_id = $1 AND template_source = 'templates' ORDER BY created_at DESC LIMIT 1 """ keywords_result = await conn.fetchrow(keywords_query, template['id']) keywords = [] if keywords_result and keywords_result['keywords_json']: keywords_data = keywords_result['keywords_json'] # Parse JSON if it's a string if isinstance(keywords_data, str): try: import json keywords = json.loads(keywords_data) except: keywords = [] elif isinstance(keywords_data, list): keywords = keywords_data print(f" ๐Ÿ”‘ Found {len(keywords)} keywords") # Create template node in Neo4j template_data = dict(template) template_data['id'] = str(template_data['id']) await neo4j_client.create_template_node(template_data) # Create tech stack node tech_stack_data = { "name": rec['stack_name'], "category": "tech_stack", "maturity_score": 0.9, "learning_curve": "medium", "performance_rating": float(rec['recommendation_score']) / 100.0 } await neo4j_client.create_technology_node(tech_stack_data) # Create recommendation relationship await neo4j_client.create_recommendation_relationship( str(template['id']), rec['stack_name'], "tech_stack", float(rec['recommendation_score']) / 100.0 ) # Create individual technology nodes and relationships tech_fields = ['frontend', 'backend', 'database', 'cloud', 'testing', 'mobile', 'devops', 'ai_ml'] for field in tech_fields: tech_value = rec[field] if tech_value and tech_value.strip(): # Parse JSON if it's a string if isinstance(tech_value, str) and tech_value.startswith('{'): try: tech_value = json.loads(tech_value) if isinstance(tech_value, dict): tech_name = tech_value.get('name', str(tech_value)) else: tech_name = str(tech_value) except: tech_name = str(tech_value) else: tech_name = str(tech_value) # Create technology node tech_data = { "name": tech_name, "category": field, "maturity_score": 0.8, "learning_curve": "medium", "performance_rating": 0.8 } await neo4j_client.create_technology_node(tech_data) # Create relationship await neo4j_client.create_recommendation_relationship( str(template['id']), tech_name, field, 0.8 ) # Create tool node for single recommended tool recommended_tool = rec.get('recommended_tool', '') if recommended_tool and recommended_tool.strip(): # Create tool node tool_data = { "name": recommended_tool, "category": "business_tool", "type": "Tool", "maturity_score": 0.8, "learning_curve": "easy", "performance_rating": 0.8 } await neo4j_client.create_technology_node(tool_data) # Create relationship await neo4j_client.create_recommendation_relationship( str(template['id']), recommended_tool, "business_tool", 0.8 ) print(f" ๐Ÿ”ง Created tool: {recommended_tool}") # Create keyword relationships if isinstance(keywords, list): print(f" ๐Ÿ”‘ Processing {len(keywords)} keywords: {keywords[:3]}...") for keyword in keywords: if keyword and keyword.strip(): await neo4j_client.create_keyword_relationship(str(template['id']), keyword) else: print(f" โš ๏ธ Keywords not in expected list format: {type(keywords)}") # Create TemplateRecommendation node with rich data recommendation_data = { 'stack_name': rec['stack_name'], 'description': template.get('description', ''), 'project_scale': 'medium', 'team_size': 3, 'experience_level': 'intermediate', 'confidence_score': int(rec['recommendation_score']), 'recommendation_reasons': [ f"Tech stack: {rec['stack_name']}", f"Score: {rec['recommendation_score']}/100", "AI-generated recommendation" ], 'key_features': [ f"Frontend: {rec.get('frontend', 'N/A')}", f"Backend: {rec.get('backend', 'N/A')}", f"Database: {rec.get('database', 'N/A')}", f"Cloud: {rec.get('cloud', 'N/A')}" ], 'estimated_development_time_months': rec.get('development_time', 3), 'complexity_level': 'medium', 'budget_range_usd': f"${rec.get('monthly_cost', 0):.0f} - ${rec.get('setup_cost', 0):.0f}", 'time_to_market_weeks': rec.get('development_time', 3) * 4, 'scalability_requirements': 'moderate', 'security_requirements': 'standard', 'success_rate_percentage': rec.get('success_rate', 85), 'user_satisfaction_score': rec.get('satisfaction', 85) } await neo4j_client.create_template_recommendation_node(str(template['id']), recommendation_data) print(f" ๐Ÿ“‹ Created TemplateRecommendation node") print(f" โœ… Successfully migrated to Neo4j") await conn.close() await neo4j_client.close() print("\n๐ŸŽ‰ MIGRATION COMPLETED!") print(f"๐Ÿ“Š Successfully migrated: {len(templates)} templates") print("๐Ÿ”— Neo4j knowledge graph created with tech stack relationships") except Exception as e: print(f"โŒ Migration failed: {e}") # ============================================================================ # MAIN EXECUTION # ============================================================================ if __name__ == "__main__": import sys if len(sys.argv) > 1 and sys.argv[1] == "migrate": # Run migration asyncio.run(migrate_to_neo4j()) elif len(sys.argv) > 2 and sys.argv[1] == "--template-id": # Generate recommendations for specific template template_id = sys.argv[2] # Configure logger to output to stderr for command line usage import logging logging.basicConfig(level=logging.ERROR, stream=sys.stderr) async def get_recommendation(): try: claude_client = ClaudeClient() result = await claude_client.get_recommendation(template_id) # Only output JSON to stdout print(json.dumps(result, default=str)) except Exception as e: error_result = { "error": str(e), "template_id": template_id } print(json.dumps(error_result)) asyncio.run(get_recommendation()) else: # Start FastAPI server uvicorn.run( app, host="0.0.0.0", port=8013, log_level="info" )