2032 lines
89 KiB
Python
2032 lines
89 KiB
Python
# 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"
|
|
)
|