1031 lines
44 KiB
Python
1031 lines
44 KiB
Python
# ================================================================================================
|
|
# ENHANCED TECH STACK SELECTOR - MIGRATED VERSION
|
|
# Uses PostgreSQL data migrated to Neo4j with proper price-based relationships
|
|
# ================================================================================================
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
from datetime import datetime
|
|
from typing import Dict, Any, Optional, List
|
|
from pydantic import BaseModel
|
|
from fastapi import FastAPI, HTTPException, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from loguru import logger
|
|
import atexit
|
|
import anthropic
|
|
from neo4j import GraphDatabase
|
|
import psycopg2
|
|
from psycopg2.extras import RealDictCursor
|
|
|
|
# ================================================================================================
|
|
# NEO4J SERVICE FOR MIGRATED DATA
|
|
# ================================================================================================
|
|
|
|
class MigratedNeo4jService:
|
|
def __init__(self, uri, user, password):
|
|
self.driver = GraphDatabase.driver(
|
|
uri,
|
|
auth=(user, password),
|
|
connection_timeout=5
|
|
)
|
|
try:
|
|
self.driver.verify_connectivity()
|
|
logger.info("✅ Migrated Neo4j Service connected successfully")
|
|
except Exception as e:
|
|
logger.error(f"❌ Neo4j connection failed: {e}")
|
|
|
|
def close(self):
|
|
self.driver.close()
|
|
|
|
def run_query(self, query: str, parameters: Optional[Dict[str, Any]] = None):
|
|
with self.driver.session() as session:
|
|
result = session.run(query, parameters or {})
|
|
return [record.data() for record in result]
|
|
|
|
def get_recommendations_by_budget(self, budget: float, domain: Optional[str] = None, preferred_techs: Optional[List[str]] = None):
|
|
"""Get recommendations based on budget using migrated data"""
|
|
# Normalize domain for better matching
|
|
normalized_domain = domain.lower().strip() if domain else None
|
|
|
|
# Create domain mapping for better matching
|
|
domain_mapping = {
|
|
'web development': ['portfolio', 'blog', 'website', 'landing', 'documentation', 'personal', 'small', 'learning', 'prototype', 'startup', 'mvp', 'api', 'e-commerce', 'online', 'marketplace', 'retail'],
|
|
'ecommerce': ['e-commerce', 'online', 'marketplace', 'retail', 'store', 'shop'],
|
|
'portfolio': ['portfolio', 'personal', 'blog', 'website'],
|
|
'blog': ['blog', 'content', 'writing', 'documentation'],
|
|
'startup': ['startup', 'mvp', 'prototype', 'small', 'business'],
|
|
'api': ['api', 'backend', 'service', 'microservice'],
|
|
'mobile': ['mobile', 'app', 'ios', 'android', 'react native', 'flutter'],
|
|
'ai': ['ai', 'ml', 'machine learning', 'artificial intelligence', 'data', 'analytics'],
|
|
'gaming': ['game', 'gaming', 'unity', 'unreal'],
|
|
'healthcare': ['healthcare', 'medical', 'health', 'patient', 'clinic'],
|
|
'finance': ['finance', 'fintech', 'banking', 'payment', 'financial'],
|
|
'education': ['education', 'learning', 'course', 'training', 'elearning']
|
|
}
|
|
|
|
# Get related domain keywords
|
|
related_keywords = []
|
|
if normalized_domain:
|
|
for key, keywords in domain_mapping.items():
|
|
if any(keyword in normalized_domain for keyword in [key] + keywords):
|
|
related_keywords.extend(keywords)
|
|
break
|
|
# If no mapping found, use the original domain
|
|
if not related_keywords:
|
|
related_keywords = [normalized_domain]
|
|
|
|
# First try to get existing tech stacks with domain filtering
|
|
existing_stacks = self.run_query("""
|
|
MATCH (s:TechStack)-[:BELONGS_TO_TIER]->(p:PriceTier)
|
|
WHERE s.monthly_cost <= $budget
|
|
AND ($domain IS NULL OR
|
|
toLower(s.name) CONTAINS $normalized_domain OR
|
|
toLower(s.description) CONTAINS $normalized_domain OR
|
|
EXISTS { MATCH (d:Domain)-[:RECOMMENDS]->(s) WHERE toLower(d.name) = $normalized_domain } OR
|
|
EXISTS { MATCH (d:Domain)-[:RECOMMENDS]->(s) WHERE toLower(d.name) CONTAINS $normalized_domain } OR
|
|
(s.recommended_domains IS NOT NULL AND ANY(rd IN s.recommended_domains WHERE
|
|
ANY(keyword IN $related_keywords WHERE toLower(rd) CONTAINS keyword))))
|
|
|
|
OPTIONAL MATCH (s)-[:USES_FRONTEND]->(frontend:Technology)
|
|
OPTIONAL MATCH (s)-[:USES_BACKEND]->(backend:Technology)
|
|
OPTIONAL MATCH (s)-[:USES_DATABASE]->(database:Technology)
|
|
OPTIONAL MATCH (s)-[:USES_CLOUD]->(cloud:Technology)
|
|
OPTIONAL MATCH (s)-[:USES_TESTING]->(testing:Technology)
|
|
OPTIONAL MATCH (s)-[:USES_MOBILE]->(mobile:Technology)
|
|
OPTIONAL MATCH (s)-[:USES_DEVOPS]->(devops:Technology)
|
|
OPTIONAL MATCH (s)-[:USES_AI_ML]->(ai_ml:Technology)
|
|
|
|
WITH s, frontend, backend, database, cloud, testing, mobile, devops, ai_ml, p,
|
|
(s.satisfaction_score * 0.4 + s.success_rate * 0.3 +
|
|
CASE WHEN $budget IS NOT NULL THEN (100 - (s.monthly_cost / $budget * 100)) * 0.3 ELSE 30 END) AS base_score
|
|
|
|
WITH s, frontend, backend, database, cloud, testing, mobile, devops, ai_ml, base_score, p,
|
|
CASE WHEN $preferred_techs IS NOT NULL THEN
|
|
size([x IN $preferred_techs WHERE
|
|
toLower(x) IN [toLower(frontend.name), toLower(backend.name), toLower(database.name),
|
|
toLower(cloud.name), toLower(testing.name), toLower(mobile.name),
|
|
toLower(devops.name), toLower(ai_ml.name)]]) * 5
|
|
ELSE 0 END AS preference_bonus
|
|
|
|
RETURN s.name AS stack_name,
|
|
s.monthly_cost AS monthly_cost,
|
|
s.setup_cost AS setup_cost,
|
|
s.team_size_range AS team_size,
|
|
s.development_time_months AS development_time,
|
|
s.satisfaction_score AS satisfaction,
|
|
s.success_rate AS success_rate,
|
|
s.price_tier AS price_tier,
|
|
s.recommended_domains AS recommended_domains,
|
|
s.description AS description,
|
|
s.pros AS pros,
|
|
s.cons AS cons,
|
|
COALESCE(frontend.name, s.frontend_tech, 'Not specified') AS frontend,
|
|
COALESCE(backend.name, s.backend_tech, 'Not specified') AS backend,
|
|
COALESCE(database.name, s.database_tech, 'Not specified') AS database,
|
|
COALESCE(cloud.name, s.cloud_tech, 'Not specified') AS cloud,
|
|
COALESCE(testing.name, s.testing_tech, 'Not specified') AS testing,
|
|
COALESCE(mobile.name, s.mobile_tech, 'Not specified') AS mobile,
|
|
COALESCE(devops.name, s.devops_tech, 'Not specified') AS devops,
|
|
COALESCE(ai_ml.name, s.ai_ml_tech, 'Not specified') AS ai_ml,
|
|
base_score + preference_bonus AS recommendation_score
|
|
ORDER BY recommendation_score DESC, s.monthly_cost ASC
|
|
LIMIT 10
|
|
""", {
|
|
"budget": budget,
|
|
"domain": domain,
|
|
"normalized_domain": normalized_domain,
|
|
"related_keywords": related_keywords,
|
|
"preferred_techs": preferred_techs or []
|
|
})
|
|
|
|
logger.info(f"🔍 Found {len(existing_stacks)} existing stacks from Neo4j with domain filtering")
|
|
|
|
if existing_stacks:
|
|
logger.info("✅ Using existing Neo4j stacks")
|
|
return existing_stacks
|
|
|
|
# If no domain-specific stacks found, try without domain filtering
|
|
logger.info("🔍 No domain-specific stacks found, trying without domain filter...")
|
|
existing_stacks_no_domain = self.run_query("""
|
|
MATCH (s:TechStack)-[:BELONGS_TO_TIER]->(p:PriceTier)
|
|
WHERE s.monthly_cost <= $budget
|
|
|
|
OPTIONAL MATCH (s)-[:USES_FRONTEND]->(frontend:Technology)
|
|
OPTIONAL MATCH (s)-[:USES_BACKEND]->(backend:Technology)
|
|
OPTIONAL MATCH (s)-[:USES_DATABASE]->(database:Technology)
|
|
OPTIONAL MATCH (s)-[:USES_CLOUD]->(cloud:Technology)
|
|
OPTIONAL MATCH (s)-[:USES_TESTING]->(testing:Technology)
|
|
OPTIONAL MATCH (s)-[:USES_MOBILE]->(mobile:Technology)
|
|
OPTIONAL MATCH (s)-[:USES_DEVOPS]->(devops:Technology)
|
|
OPTIONAL MATCH (s)-[:USES_AI_ML]->(ai_ml:Technology)
|
|
|
|
WITH s, frontend, backend, database, cloud, testing, mobile, devops, ai_ml, p,
|
|
(s.satisfaction_score * 0.4 + s.success_rate * 0.3 +
|
|
CASE WHEN $budget IS NOT NULL THEN (100 - (s.monthly_cost / $budget * 100)) * 0.3 ELSE 30 END) AS base_score
|
|
|
|
WITH s, frontend, backend, database, cloud, testing, mobile, devops, ai_ml, base_score, p,
|
|
CASE WHEN $preferred_techs IS NOT NULL THEN
|
|
size([x IN $preferred_techs WHERE
|
|
toLower(x) IN [toLower(frontend.name), toLower(backend.name), toLower(database.name),
|
|
toLower(cloud.name), toLower(testing.name), toLower(mobile.name),
|
|
toLower(devops.name), toLower(ai_ml.name)]]) * 5
|
|
ELSE 0 END AS preference_bonus
|
|
|
|
RETURN s.name AS stack_name,
|
|
s.monthly_cost AS monthly_cost,
|
|
s.setup_cost AS setup_cost,
|
|
s.team_size_range AS team_size,
|
|
s.development_time_months AS development_time,
|
|
s.satisfaction_score AS satisfaction,
|
|
s.success_rate AS success_rate,
|
|
s.price_tier AS price_tier,
|
|
s.recommended_domains AS recommended_domains,
|
|
s.description AS description,
|
|
s.pros AS pros,
|
|
s.cons AS cons,
|
|
COALESCE(frontend.name, s.frontend_tech, 'Not specified') AS frontend,
|
|
COALESCE(backend.name, s.backend_tech, 'Not specified') AS backend,
|
|
COALESCE(database.name, s.database_tech, 'Not specified') AS database,
|
|
COALESCE(cloud.name, s.cloud_tech, 'Not specified') AS cloud,
|
|
COALESCE(testing.name, s.testing_tech, 'Not specified') AS testing,
|
|
COALESCE(mobile.name, s.mobile_tech, 'Not specified') AS mobile,
|
|
COALESCE(devops.name, s.devops_tech, 'Not specified') AS devops,
|
|
COALESCE(ai_ml.name, s.ai_ml_tech, 'Not specified') AS ai_ml,
|
|
base_score + preference_bonus AS recommendation_score
|
|
ORDER BY recommendation_score DESC, s.monthly_cost ASC
|
|
LIMIT 10
|
|
""", {
|
|
"budget": budget,
|
|
"preferred_techs": preferred_techs or []
|
|
})
|
|
|
|
logger.info(f"🔍 Found {len(existing_stacks_no_domain)} existing stacks from Neo4j without domain filtering")
|
|
|
|
if existing_stacks_no_domain:
|
|
logger.info("✅ Using existing Neo4j stacks (no domain filter)")
|
|
return existing_stacks_no_domain
|
|
|
|
# If no existing stacks, try Claude AI for intelligent recommendations
|
|
logger.info("🤖 No existing stacks found, trying Claude AI...")
|
|
claude_recommendations = self.get_claude_ai_recommendations(budget, domain, preferred_techs)
|
|
if claude_recommendations:
|
|
logger.info(f"✅ Generated {len(claude_recommendations)} Claude AI recommendations")
|
|
return claude_recommendations
|
|
|
|
# Final fallback to dynamic recommendations using tools and technologies
|
|
logger.info("⚠️ Claude AI failed, falling back to dynamic recommendations")
|
|
return self.get_dynamic_recommendations(budget, domain, preferred_techs)
|
|
|
|
def get_dynamic_recommendations(self, budget: float, domain: Optional[str] = None, preferred_techs: Optional[List[str]] = None):
|
|
"""Create dynamic recommendations using tools and technologies"""
|
|
# Normalize domain for better matching
|
|
normalized_domain = domain.lower().strip() if domain else None
|
|
|
|
# Get tools within budget
|
|
tools_query = """
|
|
MATCH (tool:Tool)-[:BELONGS_TO_TIER]->(p:PriceTier)
|
|
WHERE tool.monthly_cost_usd <= $budget
|
|
RETURN tool.name as tool_name,
|
|
tool.category as category,
|
|
tool.monthly_cost_usd as monthly_cost,
|
|
tool.total_cost_of_ownership_score as tco_score,
|
|
tool.price_performance_ratio as price_performance,
|
|
p.tier_name as price_tier
|
|
ORDER BY tool.price_performance_ratio DESC, tool.monthly_cost_usd ASC
|
|
LIMIT 20
|
|
"""
|
|
|
|
tools = self.run_query(tools_query, {"budget": budget})
|
|
|
|
# Get technologies by category (without pricing constraints)
|
|
tech_categories = ["frontend", "backend", "database", "cloud", "testing", "mobile", "devops", "ai_ml"]
|
|
recommendations = []
|
|
|
|
# Create domain-specific recommendations
|
|
domain_specific_stacks = self._create_domain_specific_stacks(normalized_domain, budget)
|
|
if domain_specific_stacks:
|
|
recommendations.extend(domain_specific_stacks)
|
|
|
|
for category in tech_categories:
|
|
tech_query = f"""
|
|
MATCH (t:Technology {{category: '{category}'}})
|
|
RETURN t.name as name,
|
|
t.category as category,
|
|
t.maturity_score as maturity_score,
|
|
t.learning_curve as learning_curve,
|
|
t.performance_rating as performance_rating,
|
|
t.total_cost_of_ownership_score as tco_score,
|
|
t.price_performance_ratio as price_performance
|
|
ORDER BY t.total_cost_of_ownership_score DESC, t.maturity_score DESC
|
|
LIMIT 3
|
|
"""
|
|
|
|
technologies = self.run_query(tech_query)
|
|
|
|
if technologies:
|
|
# Create a recommendation entry for this category
|
|
best_tech = technologies[0]
|
|
recommendation = {
|
|
"stack_name": f"Dynamic {category.title()} Stack - {best_tech['name']}",
|
|
"monthly_cost": 0.0, # Technologies don't have pricing
|
|
"setup_cost": 0.0,
|
|
"team_size_range": "2-5",
|
|
"development_time_months": 2,
|
|
"satisfaction_score": best_tech.get('tco_score') or 80,
|
|
"success_rate": best_tech.get('maturity_score') or 80,
|
|
"price_tier": "Custom",
|
|
"budget_efficiency": 100.0,
|
|
"frontend": best_tech['name'] if category == 'frontend' else 'Not specified',
|
|
"backend": best_tech['name'] if category == 'backend' else 'Not specified',
|
|
"database": best_tech['name'] if category == 'database' else 'Not specified',
|
|
"cloud": best_tech['name'] if category == 'cloud' else 'Not specified',
|
|
"testing": best_tech['name'] if category == 'testing' else 'Not specified',
|
|
"mobile": best_tech['name'] if category == 'mobile' else 'Not specified',
|
|
"devops": best_tech['name'] if category == 'devops' else 'Not specified',
|
|
"ai_ml": best_tech['name'] if category == 'ai_ml' else 'Not specified',
|
|
"recommendation_score": (best_tech.get('tco_score') or 80) + (best_tech.get('maturity_score') or 80) / 2
|
|
}
|
|
recommendations.append(recommendation)
|
|
|
|
# Add tool-based recommendations
|
|
if tools:
|
|
# Group tools by category and create recommendations
|
|
tool_categories = {}
|
|
for tool in tools:
|
|
category = tool['category']
|
|
if category not in tool_categories:
|
|
tool_categories[category] = []
|
|
tool_categories[category].append(tool)
|
|
|
|
for category, category_tools in tool_categories.items():
|
|
if category_tools:
|
|
best_tool = category_tools[0]
|
|
total_cost = sum(t['monthly_cost'] for t in category_tools[:3]) # Top 3 tools
|
|
|
|
if total_cost <= budget:
|
|
recommendation = {
|
|
"stack_name": f"Tool-based {category.title()} Stack - {best_tool['tool_name']}",
|
|
"monthly_cost": total_cost,
|
|
"setup_cost": total_cost * 0.5,
|
|
"team_size_range": "1-3",
|
|
"development_time_months": 1,
|
|
"satisfaction_score": best_tool.get('tco_score') or 80,
|
|
"success_rate": best_tool.get('price_performance') or 80,
|
|
"price_tier": best_tool.get('price_tier', 'Custom'),
|
|
"budget_efficiency": 100.0 - ((total_cost / budget) * 20) if budget > 0 else 100.0,
|
|
"frontend": "Not specified",
|
|
"backend": "Not specified",
|
|
"database": "Not specified",
|
|
"cloud": "Not specified",
|
|
"testing": "Not specified",
|
|
"mobile": "Not specified",
|
|
"devops": "Not specified",
|
|
"ai_ml": "Not specified",
|
|
"recommendation_score": (best_tool.get('tco_score') or 80) + (best_tool.get('price_performance') or 80) / 2,
|
|
"tools": [t['tool_name'] for t in category_tools[:3]]
|
|
}
|
|
recommendations.append(recommendation)
|
|
|
|
# Sort by recommendation score and return top 10
|
|
recommendations.sort(key=lambda x: x['recommendation_score'], reverse=True)
|
|
return recommendations[:10]
|
|
|
|
def _create_domain_specific_stacks(self, domain: Optional[str], budget: float):
|
|
"""Create domain-specific technology stacks"""
|
|
if not domain:
|
|
return []
|
|
|
|
# Domain-specific technology mappings
|
|
domain_tech_mapping = {
|
|
'healthcare': {
|
|
'frontend': 'React',
|
|
'backend': 'Django',
|
|
'database': 'PostgreSQL',
|
|
'cloud': 'AWS',
|
|
'testing': 'Jest',
|
|
'mobile': 'React Native',
|
|
'devops': 'Docker',
|
|
'ai_ml': 'TensorFlow'
|
|
},
|
|
'finance': {
|
|
'frontend': 'Angular',
|
|
'backend': 'Java Spring',
|
|
'database': 'PostgreSQL',
|
|
'cloud': 'AWS',
|
|
'testing': 'JUnit',
|
|
'mobile': 'Flutter',
|
|
'devops': 'Kubernetes',
|
|
'ai_ml': 'Scikit-learn'
|
|
},
|
|
'gaming': {
|
|
'frontend': 'Unity',
|
|
'backend': 'Node.js',
|
|
'database': 'MongoDB',
|
|
'cloud': 'AWS',
|
|
'testing': 'Unity Test Framework',
|
|
'mobile': 'Unity',
|
|
'devops': 'Docker',
|
|
'ai_ml': 'TensorFlow'
|
|
},
|
|
'education': {
|
|
'frontend': 'React',
|
|
'backend': 'Django',
|
|
'database': 'PostgreSQL',
|
|
'cloud': 'DigitalOcean',
|
|
'testing': 'Jest',
|
|
'mobile': 'React Native',
|
|
'devops': 'Docker',
|
|
'ai_ml': 'Scikit-learn'
|
|
},
|
|
'media': {
|
|
'frontend': 'Next.js',
|
|
'backend': 'Node.js',
|
|
'database': 'MongoDB',
|
|
'cloud': 'Vercel',
|
|
'testing': 'Jest',
|
|
'mobile': 'React Native',
|
|
'devops': 'Docker',
|
|
'ai_ml': 'Hugging Face'
|
|
},
|
|
'iot': {
|
|
'frontend': 'React',
|
|
'backend': 'Python',
|
|
'database': 'InfluxDB',
|
|
'cloud': 'AWS',
|
|
'testing': 'Pytest',
|
|
'mobile': 'React Native',
|
|
'devops': 'Docker',
|
|
'ai_ml': 'TensorFlow'
|
|
},
|
|
'social': {
|
|
'frontend': 'React',
|
|
'backend': 'Node.js',
|
|
'database': 'MongoDB',
|
|
'cloud': 'AWS',
|
|
'testing': 'Jest',
|
|
'mobile': 'React Native',
|
|
'devops': 'Docker',
|
|
'ai_ml': 'Hugging Face'
|
|
},
|
|
'elearning': {
|
|
'frontend': 'Vue.js',
|
|
'backend': 'Django',
|
|
'database': 'PostgreSQL',
|
|
'cloud': 'DigitalOcean',
|
|
'testing': 'Jest',
|
|
'mobile': 'Flutter',
|
|
'devops': 'Docker',
|
|
'ai_ml': 'Scikit-learn'
|
|
},
|
|
'realestate': {
|
|
'frontend': 'React',
|
|
'backend': 'Node.js',
|
|
'database': 'PostgreSQL',
|
|
'cloud': 'AWS',
|
|
'testing': 'Jest',
|
|
'mobile': 'React Native',
|
|
'devops': 'Docker',
|
|
'ai_ml': 'Not specified'
|
|
},
|
|
'travel': {
|
|
'frontend': 'React',
|
|
'backend': 'Node.js',
|
|
'database': 'MongoDB',
|
|
'cloud': 'AWS',
|
|
'testing': 'Jest',
|
|
'mobile': 'React Native',
|
|
'devops': 'Docker',
|
|
'ai_ml': 'Not specified'
|
|
},
|
|
'manufacturing': {
|
|
'frontend': 'Angular',
|
|
'backend': 'Java Spring',
|
|
'database': 'PostgreSQL',
|
|
'cloud': 'AWS',
|
|
'testing': 'JUnit',
|
|
'mobile': 'Flutter',
|
|
'devops': 'Kubernetes',
|
|
'ai_ml': 'TensorFlow'
|
|
}
|
|
}
|
|
|
|
# Get technology mapping for domain
|
|
tech_mapping = domain_tech_mapping.get(domain)
|
|
if not tech_mapping:
|
|
return []
|
|
|
|
# Create domain-specific stack
|
|
stack = {
|
|
"stack_name": f"Domain-Specific {domain.title()} Stack",
|
|
"monthly_cost": min(budget * 0.8, 100.0), # Use 80% of budget or max $100
|
|
"setup_cost": min(budget * 0.4, 500.0), # Use 40% of budget or max $500
|
|
"team_size_range": "3-6",
|
|
"development_time_months": 4,
|
|
"satisfaction_score": 85,
|
|
"success_rate": 88,
|
|
"price_tier": "Custom",
|
|
"recommended_domains": [domain.title()],
|
|
"description": f"Specialized technology stack optimized for {domain} applications",
|
|
"pros": [
|
|
f"Optimized for {domain}",
|
|
"Domain-specific features",
|
|
"Proven technology choices",
|
|
"Good performance"
|
|
],
|
|
"cons": [
|
|
"Domain-specific complexity",
|
|
"Learning curve",
|
|
"Customization needs"
|
|
],
|
|
"frontend": tech_mapping['frontend'],
|
|
"backend": tech_mapping['backend'],
|
|
"database": tech_mapping['database'],
|
|
"cloud": tech_mapping['cloud'],
|
|
"testing": tech_mapping['testing'],
|
|
"mobile": tech_mapping['mobile'],
|
|
"devops": tech_mapping['devops'],
|
|
"ai_ml": tech_mapping['ai_ml'],
|
|
"recommendation_score": 90.0
|
|
}
|
|
|
|
return [stack]
|
|
|
|
def get_available_domains(self):
|
|
"""Get all available domains from the database"""
|
|
query = """
|
|
MATCH (d:Domain)
|
|
RETURN d.name as domain_name,
|
|
d.project_scale as project_scale,
|
|
d.team_experience_level as team_experience_level
|
|
ORDER BY d.name
|
|
"""
|
|
return self.run_query(query)
|
|
|
|
def get_technologies_by_price_tier(self, tier_name: str):
|
|
"""Get technologies for a specific price tier"""
|
|
query = """
|
|
MATCH (t:Technology)-[:BELONGS_TO_TIER]->(p:PriceTier {tier_name: $tier_name})
|
|
RETURN t.name as name,
|
|
t.category as category,
|
|
t.monthly_cost_usd as monthly_cost,
|
|
t.total_cost_of_ownership_score as tco_score,
|
|
t.price_performance_ratio as price_performance,
|
|
t.maturity_score as maturity_score,
|
|
t.learning_curve as learning_curve
|
|
ORDER BY t.total_cost_of_ownership_score DESC, t.monthly_cost_usd ASC
|
|
"""
|
|
return self.run_query(query, {"tier_name": tier_name})
|
|
|
|
def get_tools_by_price_tier(self, tier_name: str):
|
|
"""Get tools for a specific price tier"""
|
|
query = """
|
|
MATCH (tool:Tool)-[:BELONGS_TO_TIER]->(p:PriceTier {tier_name: $tier_name})
|
|
RETURN tool.name as name,
|
|
tool.category as category,
|
|
tool.monthly_cost_usd as monthly_cost,
|
|
tool.total_cost_of_ownership_score as tco_score,
|
|
tool.price_performance_ratio as price_performance,
|
|
tool.popularity_score as popularity_score
|
|
ORDER BY tool.price_performance_ratio DESC, tool.monthly_cost_usd ASC
|
|
"""
|
|
return self.run_query(query, {"tier_name": tier_name})
|
|
|
|
def get_price_tier_analysis(self):
|
|
"""Get analysis of all price tiers"""
|
|
query = """
|
|
MATCH (p:PriceTier)
|
|
OPTIONAL MATCH (p)<-[:BELONGS_TO_TIER]-(t:Technology)
|
|
OPTIONAL MATCH (p)<-[:BELONGS_TO_TIER]-(tool:Tool)
|
|
OPTIONAL MATCH (p)<-[:BELONGS_TO_TIER]-(s:TechStack)
|
|
|
|
RETURN p.tier_name as tier_name,
|
|
p.min_price_usd as min_price,
|
|
p.max_price_usd as max_price,
|
|
p.target_audience as target_audience,
|
|
p.typical_project_scale as project_scale,
|
|
count(DISTINCT t) as technology_count,
|
|
count(DISTINCT tool) as tool_count,
|
|
count(DISTINCT s) as stack_count,
|
|
avg(t.monthly_cost_usd) as avg_tech_cost,
|
|
avg(tool.monthly_cost_usd) as avg_tool_cost
|
|
ORDER BY p.min_price_usd
|
|
"""
|
|
return self.run_query(query)
|
|
|
|
def get_optimal_combinations(self, budget: float, category: str):
|
|
"""Get optimal technology combinations within budget for a category"""
|
|
query = """
|
|
MATCH (t:Technology {category: $category})-[:BELONGS_TO_TIER]->(p:PriceTier)
|
|
WHERE t.monthly_cost_usd <= $budget
|
|
RETURN t.name as name,
|
|
t.monthly_cost_usd as monthly_cost,
|
|
t.total_cost_of_ownership_score as tco_score,
|
|
t.price_performance_ratio as price_performance,
|
|
p.tier_name as price_tier,
|
|
(t.total_cost_of_ownership_score * 0.6 + t.price_performance_ratio * 0.4) as combined_score
|
|
ORDER BY combined_score DESC, t.monthly_cost_usd ASC
|
|
LIMIT 10
|
|
"""
|
|
return self.run_query(query, {"budget": budget, "category": category})
|
|
|
|
def get_compatibility_analysis(self, tech_name: str):
|
|
"""Get compatibility analysis for a specific technology"""
|
|
query = """
|
|
MATCH (t:Technology {name: $tech_name})-[r:COMPATIBLE_WITH]-(compatible:Technology)
|
|
RETURN compatible.name as compatible_tech,
|
|
compatible.category as category,
|
|
r.compatibility_score as score,
|
|
r.integration_effort as effort,
|
|
r.reason as reason
|
|
ORDER BY r.compatibility_score DESC
|
|
"""
|
|
return self.run_query(query, {"tech_name": tech_name})
|
|
|
|
def validate_data_integrity(self):
|
|
"""Validate the integrity of migrated data"""
|
|
query = """
|
|
MATCH (s:TechStack)
|
|
RETURN s.name as stack_name,
|
|
exists((s)-[:BELONGS_TO_TIER]->()) as has_price_tier,
|
|
exists((s)-[:USES_FRONTEND]->()) as has_frontend,
|
|
exists((s)-[:USES_BACKEND]->()) as has_backend,
|
|
exists((s)-[:USES_DATABASE]->()) as has_database,
|
|
exists((s)-[:USES_CLOUD]->()) as has_cloud,
|
|
s.monthly_cost as monthly_cost,
|
|
s.price_tier as price_tier
|
|
ORDER BY s.monthly_cost
|
|
"""
|
|
return self.run_query(query)
|
|
|
|
def get_claude_ai_recommendations(self, budget: float, domain: Optional[str] = None, preferred_techs: Optional[List[str]] = None):
|
|
"""Generate recommendations using Claude AI when no knowledge graph data is available"""
|
|
try:
|
|
client = anthropic.Anthropic(api_key=api_key)
|
|
|
|
# Create a comprehensive prompt for Claude AI
|
|
prompt = f"""
|
|
You are a tech stack recommendation expert. Generate 5-10 technology stack recommendations based on the following requirements:
|
|
|
|
**Requirements:**
|
|
- Budget: ${budget:,.2f} per month
|
|
- Domain: {domain or 'general'}
|
|
- Preferred Technologies: {', '.join(preferred_techs) if preferred_techs else 'None specified'}
|
|
|
|
**Output Format:**
|
|
Return a JSON array with the following structure for each recommendation:
|
|
{{
|
|
"stack_name": "Descriptive name for the tech stack",
|
|
"monthly_cost": number (monthly operational cost in USD),
|
|
"setup_cost": number (one-time setup cost in USD),
|
|
"team_size_range": "string (e.g., '1-2', '3-5', '6-10')",
|
|
"development_time_months": number (months to complete, 1-12),
|
|
"satisfaction_score": number (0-100, user satisfaction score),
|
|
"success_rate": number (0-100, project success rate),
|
|
"price_tier": "string (e.g., 'Micro Budget', 'Startup Budget', 'Enterprise')",
|
|
"budget_efficiency": number (0-100, how well it uses the budget),
|
|
"frontend": "string (specific frontend technology like 'React.js', 'Vue.js', 'Angular')",
|
|
"backend": "string (specific backend technology like 'Node.js', 'Django', 'Spring Boot')",
|
|
"database": "string (specific database like 'PostgreSQL', 'MongoDB', 'MySQL')",
|
|
"cloud": "string (specific cloud platform like 'AWS', 'DigitalOcean', 'Azure')",
|
|
"testing": "string (specific testing framework like 'Jest', 'pytest', 'Cypress')",
|
|
"mobile": "string (mobile technology like 'React Native', 'Flutter', 'Ionic' or 'None')",
|
|
"devops": "string (devops tool like 'Docker', 'GitHub Actions', 'Jenkins')",
|
|
"ai_ml": "string (AI/ML technology like 'TensorFlow', 'scikit-learn', 'PyTorch' or 'None')",
|
|
"recommendation_score": number (0-100, overall recommendation score),
|
|
"tools": ["array of specific tools and services"],
|
|
"description": "string (brief explanation of the recommendation)"
|
|
}}
|
|
|
|
**Important Guidelines:**
|
|
1. Ensure all technology fields have specific, realistic technology names (not "Not specified")
|
|
2. Monthly costs should be realistic and within budget
|
|
3. Consider the domain requirements carefully
|
|
4. Include preferred technologies when possible
|
|
5. Provide diverse recommendations (different approaches, complexity levels)
|
|
6. Make sure all numeric values are realistic and consistent
|
|
7. Focus on practical, implementable solutions
|
|
|
|
Generate recommendations that are:
|
|
- Cost-effective and within budget
|
|
- Appropriate for the domain
|
|
- Include modern, proven technologies
|
|
- Provide good value for money
|
|
- Are realistic to implement
|
|
"""
|
|
|
|
response = client.messages.create(
|
|
model="claude-3-5-sonnet-20241022",
|
|
max_tokens=4000,
|
|
temperature=0.7,
|
|
messages=[{
|
|
"role": "user",
|
|
"content": prompt
|
|
}]
|
|
)
|
|
|
|
# Parse Claude's response
|
|
content = response.content[0].text.strip()
|
|
|
|
# Extract JSON from the response
|
|
import re
|
|
json_match = re.search(r'\[.*\]', content, re.DOTALL)
|
|
if json_match:
|
|
recommendations = json.loads(json_match.group())
|
|
logger.info(f"✅ Generated {len(recommendations)} Claude AI recommendations")
|
|
return recommendations
|
|
else:
|
|
logger.warning("❌ Could not parse Claude AI response as JSON")
|
|
return []
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Claude AI recommendation failed: {e}")
|
|
return []
|
|
|
|
# ================================================================================================
|
|
# POSTGRESQL MIGRATION SERVICE (SAME AS BEFORE)
|
|
# ================================================================================================
|
|
|
|
class PostgreSQLMigrationService:
|
|
def __init__(self,
|
|
host="localhost",
|
|
port=5432,
|
|
user="pipeline_admin",
|
|
password="secure_pipeline_2024",
|
|
database="dev_pipeline"):
|
|
self.config = {
|
|
"host": host,
|
|
"port": port,
|
|
"user": user,
|
|
"password": password,
|
|
"database": database
|
|
}
|
|
self.connection = None
|
|
self.cursor = None
|
|
self.last_error: Optional[str] = None
|
|
|
|
def is_open(self) -> bool:
|
|
try:
|
|
return (
|
|
self.connection is not None and
|
|
getattr(self.connection, "closed", 1) == 0 and
|
|
self.cursor is not None and
|
|
not getattr(self.cursor, "closed", True)
|
|
)
|
|
except Exception:
|
|
return False
|
|
|
|
def connect(self):
|
|
try:
|
|
if self.is_open():
|
|
self.last_error = None
|
|
return True
|
|
self.connection = psycopg2.connect(**self.config)
|
|
self.cursor = self.connection.cursor(cursor_factory=RealDictCursor)
|
|
logger.info("Connected to PostgreSQL successfully")
|
|
self.last_error = None
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Error connecting to PostgreSQL: {e}")
|
|
self.last_error = str(e)
|
|
return False
|
|
|
|
def close(self):
|
|
try:
|
|
if self.cursor and not getattr(self.cursor, "closed", True):
|
|
self.cursor.close()
|
|
finally:
|
|
self.cursor = None
|
|
try:
|
|
if self.connection and getattr(self.connection, "closed", 1) == 0:
|
|
self.connection.close()
|
|
finally:
|
|
self.connection = None
|
|
|
|
# ================================================================================================
|
|
# FASTAPI APPLICATION
|
|
# ================================================================================================
|
|
|
|
app = FastAPI(
|
|
title="Enhanced Tech Stack Selector - Migrated Version",
|
|
description="Tech stack selector using PostgreSQL data migrated to Neo4j with price-based relationships",
|
|
version="15.0.0"
|
|
)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# ================================================================================================
|
|
# CONFIGURATION
|
|
# ================================================================================================
|
|
|
|
logger.remove()
|
|
logger.add(sys.stdout, level="INFO", format="{time} | {level} | {message}")
|
|
|
|
CLAUDE_API_KEY = "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"
|
|
|
|
if not os.getenv("CLAUDE_API_KEY") and CLAUDE_API_KEY:
|
|
os.environ["CLAUDE_API_KEY"] = CLAUDE_API_KEY
|
|
|
|
api_key = os.getenv("CLAUDE_API_KEY") or CLAUDE_API_KEY
|
|
logger.info(f"🔑 Claude API Key loaded: {api_key[:20]}..." if api_key else "❌ No Claude API Key found")
|
|
|
|
# Initialize services
|
|
NEO4J_URI = os.getenv("NEO4J_URI", "bolt://localhost:7687")
|
|
NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
|
|
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD", "password")
|
|
|
|
neo4j_service = MigratedNeo4jService(
|
|
uri=NEO4J_URI,
|
|
user=NEO4J_USER,
|
|
password=NEO4J_PASSWORD
|
|
)
|
|
|
|
postgres_migration_service = PostgreSQLMigrationService(
|
|
host=os.getenv("POSTGRES_HOST", "localhost"),
|
|
port=int(os.getenv("POSTGRES_PORT", "5432")),
|
|
user=os.getenv("POSTGRES_USER", "pipeline_admin"),
|
|
password=os.getenv("POSTGRES_PASSWORD", "secure_pipeline_2024"),
|
|
database=os.getenv("POSTGRES_DB", "dev_pipeline")
|
|
)
|
|
|
|
# ================================================================================================
|
|
# SHUTDOWN HANDLER
|
|
# ================================================================================================
|
|
|
|
@app.on_event("shutdown")
|
|
async def shutdown_event():
|
|
neo4j_service.close()
|
|
postgres_migration_service.close()
|
|
|
|
atexit.register(lambda: neo4j_service.close())
|
|
atexit.register(lambda: postgres_migration_service.close())
|
|
|
|
# ================================================================================================
|
|
# ENDPOINTS
|
|
# ================================================================================================
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
return {
|
|
"status": "healthy",
|
|
"service": "enhanced-tech-stack-selector-migrated",
|
|
"version": "15.0.0",
|
|
"features": ["migrated_neo4j", "postgresql_source", "claude_ai", "price_based_relationships"]
|
|
}
|
|
|
|
@app.get("/api/diagnostics")
|
|
async def diagnostics():
|
|
diagnostics_result = {
|
|
"service": "enhanced-tech-stack-selector-migrated",
|
|
"version": "15.0.0",
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"checks": {}
|
|
}
|
|
|
|
# Check Neo4j
|
|
neo4j_check = {"status": "unknown"}
|
|
try:
|
|
with neo4j_service.driver.session() as session:
|
|
result = session.run("MATCH (n) RETURN count(n) AS count")
|
|
node_count = result.single().get("count", 0)
|
|
neo4j_check.update({
|
|
"status": "ok",
|
|
"node_count": int(node_count)
|
|
})
|
|
except Exception as e:
|
|
neo4j_check.update({
|
|
"status": "error",
|
|
"error": str(e)
|
|
})
|
|
diagnostics_result["checks"]["neo4j"] = neo4j_check
|
|
|
|
# Check data integrity
|
|
try:
|
|
integrity = neo4j_service.validate_data_integrity()
|
|
neo4j_check["data_integrity"] = {
|
|
"total_stacks": len(integrity),
|
|
"complete_stacks": len([s for s in integrity if all([
|
|
s["has_price_tier"], s["has_frontend"], s["has_backend"],
|
|
s["has_database"], s["has_cloud"]
|
|
])])
|
|
}
|
|
except Exception as e:
|
|
neo4j_check["data_integrity"] = {"error": str(e)}
|
|
|
|
return diagnostics_result
|
|
|
|
# ================================================================================================
|
|
# RECOMMENDATION ENDPOINTS
|
|
# ================================================================================================
|
|
|
|
class RecommendBestRequest(BaseModel):
|
|
domain: Optional[str] = None
|
|
budget: Optional[float] = None
|
|
preferredTechnologies: Optional[List[str]] = None
|
|
|
|
@app.post("/recommend/best")
|
|
async def recommend_best(req: RecommendBestRequest):
|
|
"""Get recommendations using migrated data with price-based relationships"""
|
|
try:
|
|
if not req.budget or req.budget <= 0:
|
|
raise HTTPException(status_code=400, detail="Budget must be greater than 0")
|
|
|
|
recommendations = neo4j_service.get_recommendations_by_budget(
|
|
budget=req.budget,
|
|
domain=req.domain,
|
|
preferred_techs=req.preferredTechnologies
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"recommendations": recommendations,
|
|
"count": len(recommendations),
|
|
"budget": req.budget,
|
|
"domain": req.domain,
|
|
"data_source": "migrated_postgresql"
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error in recommendations: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@app.get("/api/price-tiers")
|
|
async def get_price_tiers():
|
|
"""Get all price tiers with analysis"""
|
|
try:
|
|
analysis = neo4j_service.get_price_tier_analysis()
|
|
return {
|
|
"success": True,
|
|
"price_tiers": analysis,
|
|
"count": len(analysis)
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error fetching price tiers: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@app.get("/api/technologies/{tier_name}")
|
|
async def get_technologies_by_tier(tier_name: str):
|
|
"""Get technologies for a specific price tier"""
|
|
try:
|
|
technologies = neo4j_service.get_technologies_by_price_tier(tier_name)
|
|
return {
|
|
"success": True,
|
|
"tier_name": tier_name,
|
|
"technologies": technologies,
|
|
"count": len(technologies)
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error fetching technologies for tier {tier_name}: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@app.get("/api/tools/{tier_name}")
|
|
async def get_tools_by_tier(tier_name: str):
|
|
"""Get tools for a specific price tier"""
|
|
try:
|
|
tools = neo4j_service.get_tools_by_price_tier(tier_name)
|
|
return {
|
|
"success": True,
|
|
"tier_name": tier_name,
|
|
"tools": tools,
|
|
"count": len(tools)
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error fetching tools for tier {tier_name}: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@app.get("/api/combinations/optimal")
|
|
async def get_optimal_combinations(budget: float, category: str):
|
|
"""Get optimal technology combinations within budget"""
|
|
try:
|
|
if budget <= 0:
|
|
raise HTTPException(status_code=400, detail="Budget must be greater than 0")
|
|
|
|
combinations = neo4j_service.get_optimal_combinations(budget, category)
|
|
return {
|
|
"success": True,
|
|
"combinations": combinations,
|
|
"count": len(combinations),
|
|
"budget": budget,
|
|
"category": category
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error finding optimal combinations: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@app.get("/api/compatibility/{tech_name}")
|
|
async def get_compatibility_analysis(tech_name: str):
|
|
"""Get compatibility analysis for a technology"""
|
|
try:
|
|
compatibility = neo4j_service.get_compatibility_analysis(tech_name)
|
|
return {
|
|
"success": True,
|
|
"tech_name": tech_name,
|
|
"compatible_technologies": compatibility,
|
|
"count": len(compatibility)
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error fetching compatibility for {tech_name}: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@app.get("/api/validate/integrity")
|
|
async def validate_data_integrity():
|
|
"""Validate data integrity of migrated data"""
|
|
try:
|
|
integrity = neo4j_service.validate_data_integrity()
|
|
return {
|
|
"success": True,
|
|
"integrity_check": integrity,
|
|
"summary": {
|
|
"total_stacks": len(integrity),
|
|
"complete_stacks": len([s for s in integrity if all([
|
|
s["has_price_tier"], s["has_frontend"], s["has_backend"],
|
|
s["has_database"], s["has_cloud"]
|
|
])]),
|
|
"incomplete_stacks": len([s for s in integrity if not all([
|
|
s["has_price_tier"], s["has_frontend"], s["has_backend"],
|
|
s["has_database"], s["has_cloud"]
|
|
])])
|
|
}
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error validating data integrity: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@app.get("/api/domains")
|
|
async def get_available_domains():
|
|
"""Get all available domains"""
|
|
try:
|
|
domains = neo4j_service.get_available_domains()
|
|
return {
|
|
"success": True,
|
|
"domains": domains,
|
|
"count": len(domains)
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error fetching domains: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
# ================================================================================================
|
|
# MAIN ENTRY POINT
|
|
# ================================================================================================
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
|
|
logger.info("="*60)
|
|
logger.info("🚀 ENHANCED TECH STACK SELECTOR v15.0 - MIGRATED VERSION")
|
|
logger.info("="*60)
|
|
logger.info("✅ Migrated PostgreSQL data to Neo4j")
|
|
logger.info("✅ Price-based relationships")
|
|
logger.info("✅ Real data from PostgreSQL")
|
|
logger.info("✅ Claude AI recommendations")
|
|
logger.info("✅ Comprehensive pricing analysis")
|
|
logger.info("="*60)
|
|
|
|
uvicorn.run("main_migrated:app", host="0.0.0.0", port=8002, log_level="info")
|