Commit
·
b10623f
1
Parent(s):
8fb64cb
new commit
Browse files- app.py +252 -48
- core/__pycache__/llm_providers.cpython-313.pyc +0 -0
- services/pattern_detector.py +227 -303
app.py
CHANGED
|
@@ -170,43 +170,173 @@ def extract_file_list(zip_path):
|
|
| 170 |
)
|
| 171 |
|
| 172 |
# --- TAB 1: SINGLE FILE ANALYSIS ---
|
| 173 |
-
def
|
| 174 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
if not code_snippet.strip():
|
| 176 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
try:
|
| 179 |
-
# Parse code with AST
|
| 180 |
tree = ast.parse(code_snippet)
|
| 181 |
visitor = ArchitectureVisitor()
|
| 182 |
visitor.visit(tree)
|
| 183 |
|
| 184 |
if not visitor.structure:
|
| 185 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
|
| 187 |
-
#
|
|
|
|
| 188 |
if enrich_types:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
try:
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
visitor.structure = enricher.enrich(code_snippet, visitor.structure)
|
| 194 |
-
logger.info("✓ Type enrichment complete")
|
| 195 |
except Exception as e:
|
| 196 |
logger.warning(f"Type enrichment failed: {e}")
|
| 197 |
|
| 198 |
-
# Convert to PlantUML
|
| 199 |
converter = DeterministicPlantUMLConverter()
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
|
| 203 |
-
|
| 204 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
except SyntaxError as se:
|
| 206 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
except Exception as e:
|
| 208 |
logger.error(f"Code analysis error: {e}")
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
|
| 211 |
# --- TAB 2: PROJECT MAP ---
|
| 212 |
|
|
@@ -1147,49 +1277,123 @@ with gr.Blocks(
|
|
| 1147 |
with gr.Tabs():
|
| 1148 |
|
| 1149 |
# TAB 1: Single File
|
| 1150 |
-
with gr.Tab("📄 Single File Analysis"
|
| 1151 |
-
gr.HTML('<div class="info-card"><strong>💡
|
| 1152 |
-
|
| 1153 |
-
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
|
| 1157 |
-
|
| 1158 |
-
|
| 1159 |
-
|
| 1160 |
-
|
| 1161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1162 |
enrich_checkbox = gr.Checkbox(
|
| 1163 |
-
label="✨ AI
|
| 1164 |
value=False,
|
| 1165 |
-
info="Use AI
|
| 1166 |
)
|
| 1167 |
-
|
| 1168 |
-
"
|
| 1169 |
-
|
| 1170 |
-
|
| 1171 |
-
|
| 1172 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1173 |
|
| 1174 |
-
|
| 1175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1176 |
with gr.Group(elem_classes=["output-card"]):
|
| 1177 |
-
|
| 1178 |
-
|
| 1179 |
-
label="📊 Class Diagram",
|
| 1180 |
type="pil",
|
| 1181 |
elem_classes=["diagram-container"]
|
| 1182 |
)
|
|
|
|
|
|
|
| 1183 |
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1187 |
analyze_btn.click(
|
| 1188 |
-
fn=
|
| 1189 |
-
inputs=[code_input, enrich_checkbox],
|
| 1190 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1191 |
)
|
| 1192 |
-
|
| 1193 |
|
| 1194 |
# TAB 2: Project Map
|
| 1195 |
with gr.Tab("📂 Project Map", id=1):
|
|
|
|
| 170 |
)
|
| 171 |
|
| 172 |
# --- TAB 1: SINGLE FILE ANALYSIS ---
|
| 173 |
+
def process_code_snippet_with_patterns(code_snippet: str, enrich_types: bool = False, provider: str = "sambanova"):
|
| 174 |
+
"""
|
| 175 |
+
Analyze single Python code snippet:
|
| 176 |
+
1. Detect design patterns and recommendations
|
| 177 |
+
2. Generate current UML diagram
|
| 178 |
+
3. Generate before/after diagrams for recommendations
|
| 179 |
+
"""
|
| 180 |
if not code_snippet.strip():
|
| 181 |
+
return (
|
| 182 |
+
"⚠️ Please enter some code.",
|
| 183 |
+
None, None,
|
| 184 |
+
gr.update(visible=True, value="⚠️ No Input"),
|
| 185 |
+
gr.update(visible=False),
|
| 186 |
+
gr.update(visible=False),
|
| 187 |
+
gr.update(choices=[]),
|
| 188 |
+
None, None, "", ""
|
| 189 |
+
)
|
| 190 |
|
| 191 |
try:
|
| 192 |
+
# STEP 1: Parse code with AST
|
| 193 |
tree = ast.parse(code_snippet)
|
| 194 |
visitor = ArchitectureVisitor()
|
| 195 |
visitor.visit(tree)
|
| 196 |
|
| 197 |
if not visitor.structure:
|
| 198 |
+
return (
|
| 199 |
+
"⚠️ No classes/functions found.",
|
| 200 |
+
None, None,
|
| 201 |
+
gr.update(visible=True, value="⚠️ No Structure"),
|
| 202 |
+
gr.update(visible=False),
|
| 203 |
+
gr.update(visible=False),
|
| 204 |
+
gr.update(choices=[]),
|
| 205 |
+
None, None, "", ""
|
| 206 |
+
)
|
| 207 |
|
| 208 |
+
# STEP 2: Pattern Detection
|
| 209 |
+
llm = None
|
| 210 |
if enrich_types:
|
| 211 |
+
llm = _llm_singleton.get_client(preferred_provider=provider, temperature=0.0)
|
| 212 |
+
|
| 213 |
+
service = PatternDetectionService(llm=llm)
|
| 214 |
+
pattern_result = service.analyze_code(code_snippet, enrich=enrich_types)
|
| 215 |
+
|
| 216 |
+
# Format pattern report
|
| 217 |
+
pattern_report = service.format_report(pattern_result)
|
| 218 |
+
|
| 219 |
+
# STEP 3: Generate current UML diagram
|
| 220 |
+
if enrich_types and llm:
|
| 221 |
try:
|
| 222 |
+
enricher = FastTypeEnricher(llm)
|
| 223 |
+
visitor.structure = enricher.enrich(code_snippet, visitor.structure)
|
| 224 |
+
logger.info("✓ Type enrichment complete")
|
|
|
|
|
|
|
| 225 |
except Exception as e:
|
| 226 |
logger.warning(f"Type enrichment failed: {e}")
|
| 227 |
|
|
|
|
| 228 |
converter = DeterministicPlantUMLConverter()
|
| 229 |
+
current_puml = converter.convert(visitor.structure)
|
| 230 |
+
_, current_image = render_plantuml(current_puml)
|
| 231 |
+
|
| 232 |
+
# STEP 4: Generate before/after UML for recommendations
|
| 233 |
+
recommendation_choices = []
|
| 234 |
+
first_before_img = None
|
| 235 |
+
first_after_img = None
|
| 236 |
+
first_before_uml = ""
|
| 237 |
+
first_after_uml = ""
|
| 238 |
+
|
| 239 |
+
if pattern_result['recommendations']:
|
| 240 |
+
for i, rec_dict in enumerate(pattern_result['recommendations']):
|
| 241 |
+
rec = PatternRecommendation(**rec_dict)
|
| 242 |
+
recommendation_choices.append(f"{i+1}. {rec.pattern} - {rec.location}")
|
| 243 |
+
|
| 244 |
+
# Generate UML for first recommendation
|
| 245 |
+
if i == 0:
|
| 246 |
+
recommender = service.recommender
|
| 247 |
+
before_uml, after_uml = recommender.generate_recommendation_uml(rec, visitor.structure, code_snippet)
|
| 248 |
+
|
| 249 |
+
first_before_uml = before_uml
|
| 250 |
+
first_after_uml = after_uml
|
| 251 |
+
|
| 252 |
+
# Render to images
|
| 253 |
+
_, first_before_img = render_plantuml(before_uml)
|
| 254 |
+
_, first_after_img = render_plantuml(after_uml)
|
| 255 |
+
|
| 256 |
+
# Show pattern sections if recommendations exist
|
| 257 |
+
show_patterns = len(pattern_result['recommendations']) > 0
|
| 258 |
|
| 259 |
+
status_msg = f"✅ Found {pattern_result['summary']['total_patterns']} pattern(s) • {pattern_result['summary']['total_recommendations']} recommendation(s)"
|
| 260 |
|
| 261 |
+
return (
|
| 262 |
+
pattern_report,
|
| 263 |
+
current_puml,
|
| 264 |
+
current_image,
|
| 265 |
+
gr.update(visible=True, value=status_msg),
|
| 266 |
+
gr.update(visible=show_patterns), # Pattern section
|
| 267 |
+
gr.update(visible=show_patterns), # Comparison section
|
| 268 |
+
gr.update(choices=recommendation_choices, value=recommendation_choices[0] if recommendation_choices else None),
|
| 269 |
+
first_before_img,
|
| 270 |
+
first_after_img,
|
| 271 |
+
first_before_uml,
|
| 272 |
+
first_after_uml
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
except SyntaxError as se:
|
| 276 |
+
return (
|
| 277 |
+
f"❌ Syntax Error: {se}",
|
| 278 |
+
None, None,
|
| 279 |
+
gr.update(visible=True, value="❌ Syntax Error"),
|
| 280 |
+
gr.update(visible=False),
|
| 281 |
+
gr.update(visible=False),
|
| 282 |
+
gr.update(choices=[]),
|
| 283 |
+
None, None, "", ""
|
| 284 |
+
)
|
| 285 |
except Exception as e:
|
| 286 |
logger.error(f"Code analysis error: {e}")
|
| 287 |
+
import traceback
|
| 288 |
+
error_detail = traceback.format_exc()
|
| 289 |
+
return (
|
| 290 |
+
f"❌ Error: {e}\n\nDetails:\n{error_detail[:500]}",
|
| 291 |
+
None, None,
|
| 292 |
+
gr.update(visible=True, value="❌ Failed"),
|
| 293 |
+
gr.update(visible=False),
|
| 294 |
+
gr.update(visible=False),
|
| 295 |
+
gr.update(choices=[]),
|
| 296 |
+
None, None, "", ""
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
def update_single_file_recommendation(selected_rec, code, enrich, provider):
|
| 300 |
+
"""Update UML diagrams when user selects different recommendation in single file tab"""
|
| 301 |
+
if not selected_rec or not code:
|
| 302 |
+
return None, None, "", ""
|
| 303 |
+
|
| 304 |
+
try:
|
| 305 |
+
# Parse selection
|
| 306 |
+
rec_index = int(selected_rec.split(".")[0]) - 1
|
| 307 |
+
|
| 308 |
+
# Re-analyze
|
| 309 |
+
llm = None
|
| 310 |
+
if enrich:
|
| 311 |
+
llm = _llm_singleton.get_client(preferred_provider=provider, temperature=0.0)
|
| 312 |
+
|
| 313 |
+
# Parse structure
|
| 314 |
+
tree = ast.parse(code)
|
| 315 |
+
visitor = ArchitectureVisitor()
|
| 316 |
+
visitor.visit(tree)
|
| 317 |
+
|
| 318 |
+
service = PatternDetectionService(llm=llm)
|
| 319 |
+
result = service.analyze_code(code, enrich=False)
|
| 320 |
+
|
| 321 |
+
if rec_index < len(result['recommendations']):
|
| 322 |
+
rec_dict = result['recommendations'][rec_index]
|
| 323 |
+
rec = PatternRecommendation(**rec_dict)
|
| 324 |
+
|
| 325 |
+
# Generate UML
|
| 326 |
+
recommender = service.recommender
|
| 327 |
+
before_uml, after_uml = recommender.generate_recommendation_uml(rec, visitor.structure, code)
|
| 328 |
+
|
| 329 |
+
# Render to images
|
| 330 |
+
_, before_img = render_plantuml(before_uml)
|
| 331 |
+
_, after_img = render_plantuml(after_uml)
|
| 332 |
+
|
| 333 |
+
return before_img, after_img, before_uml, after_uml
|
| 334 |
+
|
| 335 |
+
except Exception as e:
|
| 336 |
+
logger.error(f"Recommendation update error: {e}")
|
| 337 |
+
|
| 338 |
+
return None, None, "", ""
|
| 339 |
+
|
| 340 |
|
| 341 |
# --- TAB 2: PROJECT MAP ---
|
| 342 |
|
|
|
|
| 1277 |
with gr.Tabs():
|
| 1278 |
|
| 1279 |
# TAB 1: Single File
|
| 1280 |
+
with gr.Tab("📄 Single File Analysis", id=0):
|
| 1281 |
+
gr.HTML('<div class="info-card"><strong>💡 Smart Analysis:</strong> Paste Python code to detect design patterns, get recommendations, and see before/after UML visualizations.</div>')
|
| 1282 |
+
|
| 1283 |
+
# Store code for recommendation updates
|
| 1284 |
+
stored_code = gr.State("")
|
| 1285 |
+
|
| 1286 |
+
# --- SECTION 1: CODE INPUT ---
|
| 1287 |
+
with gr.Group(elem_classes=["output-card"]):
|
| 1288 |
+
gr.Markdown("### 💻 Source Code")
|
| 1289 |
+
code_input = gr.Code(
|
| 1290 |
+
language="python",
|
| 1291 |
+
label=None,
|
| 1292 |
+
lines=15,
|
| 1293 |
+
elem_classes=["code-input"],
|
| 1294 |
+
)
|
| 1295 |
+
|
| 1296 |
+
with gr.Row():
|
| 1297 |
enrich_checkbox = gr.Checkbox(
|
| 1298 |
+
label="✨ AI Enhancement",
|
| 1299 |
value=False,
|
| 1300 |
+
info="Use AI for type hints"
|
| 1301 |
)
|
| 1302 |
+
provider_choice = gr.Dropdown(
|
| 1303 |
+
choices=["sambanova", "gemini", "nebius", "openai"],
|
| 1304 |
+
value="sambanova",
|
| 1305 |
+
label="LLM Provider",
|
| 1306 |
+
scale=1
|
| 1307 |
)
|
| 1308 |
+
|
| 1309 |
+
analyze_btn = gr.Button(
|
| 1310 |
+
"🔍 Analyze Code",
|
| 1311 |
+
variant="primary",
|
| 1312 |
+
size="lg",
|
| 1313 |
+
elem_classes=["primary-button"]
|
| 1314 |
+
)
|
| 1315 |
|
| 1316 |
+
# --- SECTION 2: READ ME / RECOMMENDATIONS ---
|
| 1317 |
+
gr.Markdown("### 📖 Analysis & Recommendations")
|
| 1318 |
+
with gr.Group(elem_classes=["output-card"]):
|
| 1319 |
+
status_banner_1 = gr.Markdown(visible=False, elem_classes=["banner"])
|
| 1320 |
+
pattern_report_single = gr.Markdown(
|
| 1321 |
+
value="*Analysis report will appear here after clicking Analyze...*"
|
| 1322 |
+
)
|
| 1323 |
+
|
| 1324 |
+
# --- SECTION 3: UML CODE & DIAGRAM ---
|
| 1325 |
+
gr.Markdown("### 🎨 UML Visualization")
|
| 1326 |
+
with gr.Group(elem_classes=["output-card"]):
|
| 1327 |
+
img_output_1 = gr.Image(
|
| 1328 |
+
label="Class Structure",
|
| 1329 |
+
type="pil",
|
| 1330 |
+
elem_classes=["diagram-container"]
|
| 1331 |
+
)
|
| 1332 |
+
with gr.Accordion("📝 PlantUML Source Code", open=False):
|
| 1333 |
+
text_output_1 = gr.Code(language="markdown", lines=10, label="UML Code")
|
| 1334 |
+
|
| 1335 |
+
# --- HIDDEN SECTION: PATTERN VISUALIZATION (Appears on demand) ---
|
| 1336 |
+
with gr.Row(visible=False) as pattern_uml_section_single:
|
| 1337 |
+
gr.HTML('<div class="info-card" style="margin-top: 2rem;"><strong>💡 Recommended Improvements:</strong> Visual comparison showing how design patterns can improve your code structure.</div>')
|
| 1338 |
+
|
| 1339 |
+
with gr.Row(visible=False) as pattern_selector_section_single:
|
| 1340 |
+
recommendation_dropdown_single = gr.Dropdown(
|
| 1341 |
+
label="📋 Select Recommendation to Visualize",
|
| 1342 |
+
choices=[],
|
| 1343 |
+
interactive=True
|
| 1344 |
+
)
|
| 1345 |
+
|
| 1346 |
+
with gr.Row(visible=False) as pattern_comparison_section_single:
|
| 1347 |
+
with gr.Column(scale=1):
|
| 1348 |
+
gr.Markdown("#### ⚠️ Before (Current Structure)")
|
| 1349 |
with gr.Group(elem_classes=["output-card"]):
|
| 1350 |
+
pattern_before_img_single = gr.Image(
|
| 1351 |
+
label="Current Design",
|
|
|
|
| 1352 |
type="pil",
|
| 1353 |
elem_classes=["diagram-container"]
|
| 1354 |
)
|
| 1355 |
+
with gr.Accordion("📝 PlantUML Code", open=False):
|
| 1356 |
+
pattern_before_uml_single = gr.Code(language="markdown", lines=8)
|
| 1357 |
|
| 1358 |
+
with gr.Column(scale=1):
|
| 1359 |
+
gr.Markdown("#### ✅ After (Recommended Pattern)")
|
| 1360 |
+
with gr.Group(elem_classes=["output-card"]):
|
| 1361 |
+
pattern_after_img_single = gr.Image(
|
| 1362 |
+
label="Improved Design",
|
| 1363 |
+
type="pil",
|
| 1364 |
+
elem_classes=["diagram-container"]
|
| 1365 |
+
)
|
| 1366 |
+
with gr.Accordion("📝 PlantUML Code", open=False):
|
| 1367 |
+
pattern_after_uml_single = gr.Code(language="markdown", lines=8)
|
| 1368 |
+
|
| 1369 |
+
# Event handlers
|
| 1370 |
analyze_btn.click(
|
| 1371 |
+
fn=process_code_snippet_with_patterns,
|
| 1372 |
+
inputs=[code_input, enrich_checkbox, provider_choice],
|
| 1373 |
+
outputs=[
|
| 1374 |
+
pattern_report_single,
|
| 1375 |
+
text_output_1,
|
| 1376 |
+
img_output_1,
|
| 1377 |
+
status_banner_1,
|
| 1378 |
+
pattern_uml_section_single,
|
| 1379 |
+
pattern_comparison_section_single,
|
| 1380 |
+
recommendation_dropdown_single,
|
| 1381 |
+
pattern_before_img_single,
|
| 1382 |
+
pattern_after_img_single,
|
| 1383 |
+
pattern_before_uml_single,
|
| 1384 |
+
pattern_after_uml_single
|
| 1385 |
+
]
|
| 1386 |
+
).then(
|
| 1387 |
+
fn=lambda x: x,
|
| 1388 |
+
inputs=code_input,
|
| 1389 |
+
outputs=stored_code
|
| 1390 |
+
)
|
| 1391 |
+
|
| 1392 |
+
recommendation_dropdown_single.change(
|
| 1393 |
+
fn=update_single_file_recommendation,
|
| 1394 |
+
inputs=[recommendation_dropdown_single, stored_code, enrich_checkbox, provider_choice],
|
| 1395 |
+
outputs=[pattern_before_img_single, pattern_after_img_single, pattern_before_uml_single, pattern_after_uml_single]
|
| 1396 |
)
|
|
|
|
| 1397 |
|
| 1398 |
# TAB 2: Project Map
|
| 1399 |
with gr.Tab("📂 Project Map", id=1):
|
core/__pycache__/llm_providers.cpython-313.pyc
CHANGED
|
Binary files a/core/__pycache__/llm_providers.cpython-313.pyc and b/core/__pycache__/llm_providers.cpython-313.pyc differ
|
|
|
services/pattern_detector.py
CHANGED
|
@@ -340,7 +340,7 @@ class PatternDetectorAST(ast.NodeVisitor):
|
|
| 340 |
class PatternRecommender:
|
| 341 |
"""
|
| 342 |
Analyzes code structure to recommend where patterns would help.
|
| 343 |
-
Uses
|
| 344 |
"""
|
| 345 |
|
| 346 |
def __init__(self, llm=None):
|
|
@@ -360,153 +360,117 @@ class PatternRecommender:
|
|
| 360 |
|
| 361 |
def generate_recommendation_uml(self, recommendation: PatternRecommendation, structure: List[Dict], code: str) -> tuple[str, str]:
|
| 362 |
"""
|
| 363 |
-
Generate before/after UML for a recommendation.
|
| 364 |
Returns: (before_uml, after_uml)
|
| 365 |
"""
|
| 366 |
if self.llm:
|
|
|
|
| 367 |
return self._generate_ai_uml(recommendation, structure, code)
|
| 368 |
else:
|
| 369 |
-
|
|
|
|
| 370 |
|
| 371 |
-
def
|
| 372 |
-
"""Generate
|
| 373 |
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
before = self._factory_before_uml(rec, structure)
|
| 381 |
-
after = self._factory_after_uml(rec)
|
| 382 |
-
return before, after
|
| 383 |
-
|
| 384 |
-
elif rec.pattern == "Singleton":
|
| 385 |
-
before = self._singleton_before_uml(rec, structure)
|
| 386 |
-
after = self._singleton_after_uml(rec)
|
| 387 |
-
return before, after
|
| 388 |
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
return before, after
|
| 393 |
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
def _strategy_before_uml(self, rec: PatternRecommendation, structure: List[Dict]) -> str:
|
| 397 |
-
"""Generate BEFORE UML for Strategy pattern recommendation"""
|
| 398 |
-
class_name = rec.location
|
| 399 |
|
| 400 |
-
|
| 401 |
-
|
|
|
|
| 402 |
skinparam classAttributeIconSize 0
|
| 403 |
|
| 404 |
class {class_name} {{
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
|
|
|
| 410 |
|
| 411 |
note right of {class_name}
|
| 412 |
-
❌
|
| 413 |
-
|
| 414 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
end note
|
| 416 |
|
| 417 |
@enduml"""
|
| 418 |
-
|
| 419 |
-
def _strategy_after_uml(self, rec: PatternRecommendation) -> str:
|
| 420 |
-
"""Generate AFTER UML for Strategy pattern recommendation"""
|
| 421 |
-
class_name = rec.location
|
| 422 |
|
| 423 |
-
|
| 424 |
-
|
|
|
|
| 425 |
skinparam classAttributeIconSize 0
|
| 426 |
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
}
|
| 440 |
-
|
| 441 |
-
class StrategyB {{
|
| 442 |
-
+ process(data)
|
| 443 |
-
}}
|
| 444 |
-
|
| 445 |
-
class StrategyC {{
|
| 446 |
-
+ process(data)
|
| 447 |
-
}}
|
| 448 |
-
|
| 449 |
-
ProcessingStrategy <|.. StrategyA
|
| 450 |
-
ProcessingStrategy <|.. StrategyB
|
| 451 |
-
ProcessingStrategy <|.. StrategyC
|
| 452 |
-
{class_name} o-- ProcessingStrategy
|
| 453 |
-
|
| 454 |
-
note right of ProcessingStrategy
|
| 455 |
-
✅ Easy to add new strategies
|
| 456 |
-
✅ Each strategy is independent
|
| 457 |
-
�� Follows Open/Closed Principle
|
| 458 |
-
✅ ~{rec.complexity_reduction}% complexity reduction
|
| 459 |
end note
|
| 460 |
|
| 461 |
@enduml"""
|
|
|
|
|
|
|
| 462 |
|
| 463 |
-
def
|
| 464 |
-
"""Generate
|
| 465 |
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
class ClientCode {{
|
| 471 |
-
+ method1()
|
| 472 |
-
+ method2()
|
| 473 |
-
+ method3()
|
| 474 |
}}
|
| 475 |
|
| 476 |
-
class
|
|
|
|
|
|
|
|
|
|
| 477 |
}}
|
| 478 |
|
| 479 |
-
class
|
|
|
|
| 480 |
}}
|
| 481 |
|
| 482 |
-
class
|
|
|
|
| 483 |
}}
|
| 484 |
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
note right of ClientCode
|
| 490 |
-
❌ Object creation scattered everywhere
|
| 491 |
-
❌ Hard to change instantiation logic
|
| 492 |
-
❌ Tight coupling to concrete classes
|
| 493 |
-
end note
|
| 494 |
-
|
| 495 |
-
@enduml"""
|
| 496 |
-
|
| 497 |
-
def _factory_after_uml(self, rec: PatternRecommendation) -> str:
|
| 498 |
-
"""Generate AFTER UML for Factory pattern recommendation"""
|
| 499 |
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
class ProductFactory {{
|
| 505 |
-
+ create_product(type: str): Product
|
| 506 |
}}
|
| 507 |
|
| 508 |
-
|
| 509 |
-
+
|
| 510 |
}}
|
| 511 |
|
| 512 |
class ProductA {{
|
|
@@ -517,244 +481,208 @@ class ProductB {{
|
|
| 517 |
+ operation()
|
| 518 |
}}
|
| 519 |
|
| 520 |
-
class ProductC {{
|
| 521 |
-
+ operation()
|
| 522 |
-
}}
|
| 523 |
-
|
| 524 |
-
class ClientCode {{
|
| 525 |
-
- factory: ProductFactory
|
| 526 |
-
+ use_product(type: str)
|
| 527 |
-
}}
|
| 528 |
-
|
| 529 |
Product <|.. ProductA
|
| 530 |
Product <|.. ProductB
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
note right of ProductFactory
|
| 536 |
-
✅ Centralized creation logic
|
| 537 |
-
✅ Easy to add new products
|
| 538 |
-
✅ Client decoupled from concrete classes
|
| 539 |
-
✅ ~{rec.complexity_reduction}% complexity reduction
|
| 540 |
-
end note
|
| 541 |
-
|
| 542 |
-
@enduml"""
|
| 543 |
-
|
| 544 |
-
def _singleton_before_uml(self, rec: PatternRecommendation, structure: List[Dict]) -> str:
|
| 545 |
-
"""Generate BEFORE UML for Singleton pattern recommendation"""
|
| 546 |
-
|
| 547 |
-
return f"""@startuml
|
| 548 |
-
title Before: Multiple Instances Possible
|
| 549 |
-
skinparam classAttributeIconSize 0
|
| 550 |
-
|
| 551 |
-
class SharedResource {{
|
| 552 |
-
+ config: Dict
|
| 553 |
-
+ connect()
|
| 554 |
-
+ disconnect()
|
| 555 |
-
}}
|
| 556 |
-
|
| 557 |
-
class ClientA {{
|
| 558 |
-
- resource: SharedResource
|
| 559 |
-
}}
|
| 560 |
-
|
| 561 |
-
class ClientB {{
|
| 562 |
-
- resource: SharedResource
|
| 563 |
-
}}
|
| 564 |
-
|
| 565 |
-
class ClientC {{
|
| 566 |
-
- resource: SharedResource
|
| 567 |
-
}}
|
| 568 |
-
|
| 569 |
-
ClientA ..> SharedResource : creates new instance
|
| 570 |
-
ClientB ..> SharedResource : creates new instance
|
| 571 |
-
ClientC ..> SharedResource : creates new instance
|
| 572 |
-
|
| 573 |
-
note right of SharedResource
|
| 574 |
-
❌ Multiple instances created
|
| 575 |
-
❌ Inconsistent state
|
| 576 |
-
❌ Resource waste
|
| 577 |
-
end note
|
| 578 |
-
|
| 579 |
-
@enduml"""
|
| 580 |
-
|
| 581 |
-
def _singleton_after_uml(self, rec: PatternRecommendation) -> str:
|
| 582 |
-
"""Generate AFTER UML for Singleton pattern recommendation"""
|
| 583 |
-
|
| 584 |
-
return """@startuml
|
| 585 |
-
title After: Singleton Pattern Applied
|
| 586 |
-
skinparam classAttributeIconSize 0
|
| 587 |
-
|
| 588 |
-
class SharedResource {{
|
| 589 |
-
- {static} _instance: SharedResource
|
| 590 |
-
- config: Dict
|
| 591 |
-
+ {static} get_instance(): SharedResource
|
| 592 |
-
+ connect()
|
| 593 |
-
+ disconnect()
|
| 594 |
-
}}
|
| 595 |
-
|
| 596 |
-
class ClientA {{
|
| 597 |
-
}}
|
| 598 |
-
|
| 599 |
-
class ClientB {{
|
| 600 |
-
}}
|
| 601 |
-
|
| 602 |
-
class ClientC {{
|
| 603 |
-
}}
|
| 604 |
-
|
| 605 |
-
ClientA ..> SharedResource : uses singleton
|
| 606 |
-
ClientB ..> SharedResource : uses singleton
|
| 607 |
-
ClientC ..> SharedResource : uses singleton
|
| 608 |
-
|
| 609 |
-
note right of SharedResource
|
| 610 |
-
✅ Single instance guaranteed
|
| 611 |
-
✅ Global access point
|
| 612 |
-
✅ Consistent state
|
| 613 |
-
✅ ~{rec.complexity_reduction}% complexity reduction
|
| 614 |
-
end note
|
| 615 |
-
|
| 616 |
-
@enduml"""
|
| 617 |
-
|
| 618 |
-
def _observer_before_uml(self, rec: PatternRecommendation, structure: List[Dict]) -> str:
|
| 619 |
-
"""Generate BEFORE UML for Observer pattern recommendation"""
|
| 620 |
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
+
|
| 628 |
-
+ notify_c()
|
| 629 |
-
}}
|
| 630 |
-
|
| 631 |
-
class ListenerA {{
|
| 632 |
-
+ update()
|
| 633 |
-
}}
|
| 634 |
-
|
| 635 |
-
class ListenerB {{
|
| 636 |
-
+ update()
|
| 637 |
-
}}
|
| 638 |
-
|
| 639 |
-
class ListenerC {{
|
| 640 |
-
+ update()
|
| 641 |
-
}}
|
| 642 |
-
|
| 643 |
-
EventSource --> ListenerA
|
| 644 |
-
EventSource --> ListenerB
|
| 645 |
-
EventSource --> ListenerC
|
| 646 |
-
|
| 647 |
-
note right of EventSource
|
| 648 |
-
❌ Tightly coupled to all listeners
|
| 649 |
-
❌ Hard to add/remove listeners
|
| 650 |
-
❌ Manual notification logic
|
| 651 |
-
end note
|
| 652 |
-
|
| 653 |
-
@enduml"""
|
| 654 |
-
|
| 655 |
-
def _observer_after_uml(self, rec: PatternRecommendation) -> str:
|
| 656 |
-
"""Generate AFTER UML for Observer pattern recommendation"""
|
| 657 |
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
skinparam classAttributeIconSize 0
|
| 661 |
-
|
| 662 |
-
interface Observer {{
|
| 663 |
+ update(event)
|
| 664 |
}}
|
| 665 |
|
| 666 |
-
class
|
| 667 |
- observers: List<Observer>
|
| 668 |
+ attach(o: Observer)
|
| 669 |
+ detach(o: Observer)
|
| 670 |
+ notify()
|
| 671 |
}}
|
| 672 |
|
| 673 |
-
class
|
| 674 |
+ update(event)
|
| 675 |
}}
|
| 676 |
|
| 677 |
-
class
|
| 678 |
+ update(event)
|
| 679 |
}}
|
| 680 |
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
}
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
note right of Subject
|
| 691 |
-
✅ Loosely coupled
|
| 692 |
-
✅ Dynamic listener management
|
| 693 |
-
✅ Automatic notifications
|
| 694 |
-
✅ ~{rec.complexity_reduction}% complexity reduction
|
| 695 |
-
end note
|
| 696 |
-
|
| 697 |
-
@enduml"""
|
| 698 |
|
| 699 |
def _generate_ai_uml(self, rec: PatternRecommendation, structure: List[Dict], code: str) -> tuple[str, str]:
|
| 700 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 701 |
try:
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 715 |
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
}}
|
| 721 |
|
| 722 |
-
|
| 723 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 724 |
"""
|
| 725 |
|
| 726 |
messages = [
|
| 727 |
-
SystemMessage(content="You are a UML
|
| 728 |
HumanMessage(content=prompt)
|
| 729 |
]
|
| 730 |
|
|
|
|
| 731 |
response = self.llm.invoke(messages)
|
| 732 |
content = response.content.strip()
|
| 733 |
|
| 734 |
-
# Clean
|
| 735 |
if "```json" in content:
|
| 736 |
content = content.split("```json")[1].split("```")[0].strip()
|
| 737 |
elif "```" in content:
|
| 738 |
content = content.split("```")[1].split("```")[0].strip()
|
| 739 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 740 |
result = json.loads(content)
|
| 741 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 742 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 743 |
except Exception as e:
|
| 744 |
-
logger.warning(f"
|
| 745 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 746 |
|
|
|
|
| 747 |
def _recommend_strategy(self, structure: List[Dict], code: str):
|
| 748 |
"""Recommend Strategy pattern for complex conditionals"""
|
| 749 |
-
# Look for classes with many if-else statements
|
| 750 |
for cls in structure:
|
| 751 |
if cls.get("type") == "module":
|
| 752 |
continue
|
| 753 |
|
| 754 |
-
# Count conditionals in methods (simplified heuristic)
|
| 755 |
conditional_count = code.count("if ") + code.count("elif ")
|
| 756 |
|
| 757 |
-
if conditional_count > 5:
|
| 758 |
self.recommendations.append(PatternRecommendation(
|
| 759 |
pattern="Strategy",
|
| 760 |
location=cls["name"],
|
|
@@ -770,7 +698,6 @@ OUTPUT ONLY VALID JSON.
|
|
| 770 |
if cls.get("type") == "module":
|
| 771 |
continue
|
| 772 |
|
| 773 |
-
# Look for multiple object instantiations
|
| 774 |
if "(" in code and code.count("= ") > 3:
|
| 775 |
self.recommendations.append(PatternRecommendation(
|
| 776 |
pattern="Factory",
|
|
@@ -783,7 +710,6 @@ OUTPUT ONLY VALID JSON.
|
|
| 783 |
|
| 784 |
def _recommend_singleton(self, structure: List[Dict], code: str):
|
| 785 |
"""Recommend Singleton for shared resources"""
|
| 786 |
-
# Look for global variables or module-level instances
|
| 787 |
if "global " in code or code.count("_instance") > 0:
|
| 788 |
self.recommendations.append(PatternRecommendation(
|
| 789 |
pattern="Singleton",
|
|
@@ -796,7 +722,6 @@ OUTPUT ONLY VALID JSON.
|
|
| 796 |
|
| 797 |
def _recommend_observer(self, structure: List[Dict], code: str):
|
| 798 |
"""Recommend Observer for event handling"""
|
| 799 |
-
# Look for callback patterns or manual notification
|
| 800 |
if "callback" in code.lower() or "notify" in code.lower():
|
| 801 |
self.recommendations.append(PatternRecommendation(
|
| 802 |
pattern="Observer",
|
|
@@ -807,7 +732,6 @@ OUTPUT ONLY VALID JSON.
|
|
| 807 |
implementation_hint="Create Subject class with attach/detach/notify methods"
|
| 808 |
))
|
| 809 |
|
| 810 |
-
|
| 811 |
class PatternEnricher:
|
| 812 |
"""
|
| 813 |
Uses AI to enrich pattern detections with:
|
|
|
|
| 340 |
class PatternRecommender:
|
| 341 |
"""
|
| 342 |
Analyzes code structure to recommend where patterns would help.
|
| 343 |
+
Uses AI to generate custom before/after UML diagrams based on actual code.
|
| 344 |
"""
|
| 345 |
|
| 346 |
def __init__(self, llm=None):
|
|
|
|
| 360 |
|
| 361 |
def generate_recommendation_uml(self, recommendation: PatternRecommendation, structure: List[Dict], code: str) -> tuple[str, str]:
|
| 362 |
"""
|
| 363 |
+
Generate before/after UML for a recommendation using AI analysis of actual code.
|
| 364 |
Returns: (before_uml, after_uml)
|
| 365 |
"""
|
| 366 |
if self.llm:
|
| 367 |
+
logger.info(f"🤖 Using AI to analyze {recommendation.pattern} pattern for {recommendation.location}")
|
| 368 |
return self._generate_ai_uml(recommendation, structure, code)
|
| 369 |
else:
|
| 370 |
+
logger.info(f"📊 Using structure-based UML for {recommendation.pattern} pattern")
|
| 371 |
+
return self._generate_structure_based_uml(recommendation, structure, code)
|
| 372 |
|
| 373 |
+
def _generate_structure_based_uml(self, rec: PatternRecommendation, structure: List[Dict], code: str) -> tuple[str, str]:
|
| 374 |
+
"""Generate UML from actual code structure when AI is not available"""
|
| 375 |
|
| 376 |
+
# Extract actual class from structure
|
| 377 |
+
target_class = None
|
| 378 |
+
for item in structure:
|
| 379 |
+
if item.get("name") == rec.location or rec.location in item.get("name", ""):
|
| 380 |
+
target_class = item
|
| 381 |
+
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
|
| 383 |
+
if not target_class:
|
| 384 |
+
# Fallback to first class if location not found
|
| 385 |
+
target_class = structure[0] if structure else {"name": rec.location, "methods": []}
|
|
|
|
| 386 |
|
| 387 |
+
class_name = target_class.get("name", rec.location)
|
| 388 |
+
methods = target_class.get("methods", [])
|
|
|
|
|
|
|
|
|
|
| 389 |
|
| 390 |
+
# Generate BEFORE diagram with actual code
|
| 391 |
+
before = f"""@startuml
|
| 392 |
+
title Before: {class_name} - Current Implementation
|
| 393 |
skinparam classAttributeIconSize 0
|
| 394 |
|
| 395 |
class {class_name} {{
|
| 396 |
+
"""
|
| 397 |
+
# Add actual methods from the class
|
| 398 |
+
for method in methods[:6]: # Show up to 6 methods
|
| 399 |
+
before += f" + {method}()\n"
|
| 400 |
+
|
| 401 |
+
before += f"""}}
|
| 402 |
|
| 403 |
note right of {class_name}
|
| 404 |
+
❌ Current Issue:
|
| 405 |
+
{rec.reason}
|
| 406 |
+
|
| 407 |
+
❌ Problems:
|
| 408 |
+
- Hard to extend
|
| 409 |
+
- High complexity
|
| 410 |
+
- Maintenance burden
|
| 411 |
+
|
| 412 |
+
Location: {rec.location}
|
| 413 |
end note
|
| 414 |
|
| 415 |
@enduml"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
|
| 417 |
+
# Generate AFTER diagram with pattern applied
|
| 418 |
+
after = f"""@startuml
|
| 419 |
+
title After: {rec.pattern} Pattern Applied to {class_name}
|
| 420 |
skinparam classAttributeIconSize 0
|
| 421 |
|
| 422 |
+
{self._generate_pattern_structure(rec.pattern, class_name, methods)}
|
| 423 |
+
|
| 424 |
+
note bottom
|
| 425 |
+
✅ Benefits:
|
| 426 |
+
{rec.benefit}
|
| 427 |
+
|
| 428 |
+
✅ Improvements:
|
| 429 |
+
- ~{rec.complexity_reduction}% complexity reduction
|
| 430 |
+
- Better separation of concerns
|
| 431 |
+
- Easier to extend and maintain
|
| 432 |
+
|
| 433 |
+
Pattern: {rec.pattern}
|
| 434 |
+
Implementation: {rec.implementation_hint}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
end note
|
| 436 |
|
| 437 |
@enduml"""
|
| 438 |
+
|
| 439 |
+
return before, after
|
| 440 |
|
| 441 |
+
def _generate_pattern_structure(self, pattern: str, class_name: str, methods: List[str]) -> str:
|
| 442 |
+
"""Generate pattern-specific structure using actual class name"""
|
| 443 |
|
| 444 |
+
if pattern == "Strategy":
|
| 445 |
+
return f"""interface Strategy {{
|
| 446 |
+
+ execute()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
}}
|
| 448 |
|
| 449 |
+
class {class_name} {{
|
| 450 |
+
- strategy: Strategy
|
| 451 |
+
+ set_strategy(s: Strategy)
|
| 452 |
+
+ execute()
|
| 453 |
}}
|
| 454 |
|
| 455 |
+
class StrategyA {{
|
| 456 |
+
+ execute()
|
| 457 |
}}
|
| 458 |
|
| 459 |
+
class StrategyB {{
|
| 460 |
+
+ execute()
|
| 461 |
}}
|
| 462 |
|
| 463 |
+
Strategy <|.. StrategyA
|
| 464 |
+
Strategy <|.. StrategyB
|
| 465 |
+
{class_name} o-- Strategy"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
|
| 467 |
+
elif pattern == "Factory":
|
| 468 |
+
return f"""interface Product {{
|
| 469 |
+
+ operation()
|
|
|
|
|
|
|
|
|
|
| 470 |
}}
|
| 471 |
|
| 472 |
+
class {class_name}Factory {{
|
| 473 |
+
+ create_product(type: str): Product
|
| 474 |
}}
|
| 475 |
|
| 476 |
class ProductA {{
|
|
|
|
| 481 |
+ operation()
|
| 482 |
}}
|
| 483 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
Product <|.. ProductA
|
| 485 |
Product <|.. ProductB
|
| 486 |
+
{class_name}Factory ..> Product
|
| 487 |
+
{class_name}Factory ..> ProductA
|
| 488 |
+
{class_name}Factory ..> ProductB"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
|
| 490 |
+
elif pattern == "Singleton":
|
| 491 |
+
return f"""class {class_name} <<Singleton>> {{
|
| 492 |
+
- {{static}} _instance: {class_name}
|
| 493 |
+
- __init__()
|
| 494 |
+
+ {{static}} get_instance(): {class_name}
|
| 495 |
+
"""
|
| 496 |
+
+ "\n".join(f" + {m}()" for m in methods[:4]) + "\n}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
|
| 498 |
+
elif pattern == "Observer":
|
| 499 |
+
return f"""interface Observer {{
|
|
|
|
|
|
|
|
|
|
| 500 |
+ update(event)
|
| 501 |
}}
|
| 502 |
|
| 503 |
+
class {class_name} {{
|
| 504 |
- observers: List<Observer>
|
| 505 |
+ attach(o: Observer)
|
| 506 |
+ detach(o: Observer)
|
| 507 |
+ notify()
|
| 508 |
}}
|
| 509 |
|
| 510 |
+
class ObserverA {{
|
| 511 |
+ update(event)
|
| 512 |
}}
|
| 513 |
|
| 514 |
+
class ObserverB {{
|
| 515 |
+ update(event)
|
| 516 |
}}
|
| 517 |
|
| 518 |
+
Observer <|.. ObserverA
|
| 519 |
+
Observer <|.. ObserverB
|
| 520 |
+
{class_name} o-- Observer"""
|
| 521 |
+
|
| 522 |
+
else:
|
| 523 |
+
# Generic pattern structure
|
| 524 |
+
return f"""class {class_name}Improved {{
|
| 525 |
+
"""
|
| 526 |
+
+ "\n".join(f" + {m}()" for m in methods[:4]) + "\n}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
|
| 528 |
def _generate_ai_uml(self, rec: PatternRecommendation, structure: List[Dict], code: str) -> tuple[str, str]:
|
| 529 |
+
"""
|
| 530 |
+
🤖 AI-POWERED UML GENERATION
|
| 531 |
+
|
| 532 |
+
This is the key method that makes diagrams show YOUR actual code,
|
| 533 |
+
not generic templates.
|
| 534 |
+
"""
|
| 535 |
try:
|
| 536 |
+
# Extract relevant code section for the target class
|
| 537 |
+
relevant_code = self._extract_relevant_code(rec.location, code)
|
| 538 |
+
|
| 539 |
+
# Get actual class details from structure
|
| 540 |
+
target_class = None
|
| 541 |
+
for item in structure:
|
| 542 |
+
if item.get("name") == rec.location:
|
| 543 |
+
target_class = item
|
| 544 |
+
break
|
| 545 |
+
|
| 546 |
+
# Prepare context for AI
|
| 547 |
+
context = {
|
| 548 |
+
"class_name": rec.location,
|
| 549 |
+
"pattern": rec.pattern,
|
| 550 |
+
"reason": rec.reason,
|
| 551 |
+
"benefit": rec.benefit,
|
| 552 |
+
"complexity_reduction": rec.complexity_reduction,
|
| 553 |
+
"code_snippet": relevant_code[:1500],
|
| 554 |
+
"class_details": target_class if target_class else {},
|
| 555 |
+
"all_classes": [item.get("name") for item in structure if item.get("type") == "class"][:10]
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
prompt = f"""You are an expert software architect analyzing REAL Python code.
|
| 559 |
|
| 560 |
+
**ACTUAL CODE TO ANALYZE:**
|
| 561 |
+
```python
|
| 562 |
+
{context['code_snippet']}
|
| 563 |
+
```
|
|
|
|
| 564 |
|
| 565 |
+
**CODE CONTEXT:**
|
| 566 |
+
- Target class: `{context['class_name']}`
|
| 567 |
+
- All classes in project: {', '.join(context['all_classes'])}
|
| 568 |
+
- Current problem: {context['reason']}
|
| 569 |
+
- Recommended pattern: {context['pattern']}
|
| 570 |
+
|
| 571 |
+
**YOUR TASK:**
|
| 572 |
+
Generate TWO PlantUML class diagrams showing before/after applying {context['pattern']} pattern.
|
| 573 |
+
|
| 574 |
+
**CRITICAL REQUIREMENTS:**
|
| 575 |
+
1. Use ACTUAL class names from the code above (e.g., `{context['class_name']}`, not "ClientCode")
|
| 576 |
+
2. Use ACTUAL method names you see in the code
|
| 577 |
+
3. Show REAL relationships between classes
|
| 578 |
+
4. BEFORE diagram: Show current problematic structure
|
| 579 |
+
5. AFTER diagram: Show improved structure with {context['pattern']} pattern
|
| 580 |
+
|
| 581 |
+
**DIAGRAM GUIDELINES:**
|
| 582 |
+
- Keep simple: 4-6 classes maximum
|
| 583 |
+
- Use actual names (no "ProductA", "ServiceA" generic names)
|
| 584 |
+
- Add notes explaining problems (BEFORE) and benefits (AFTER)
|
| 585 |
+
- Include complexity reduction: ~{context['complexity_reduction']}%
|
| 586 |
+
|
| 587 |
+
**OUTPUT FORMAT (CRITICAL):**
|
| 588 |
+
Return ONLY this JSON structure, nothing else:
|
| 589 |
+
{{{{
|
| 590 |
+
"before": "@startuml\\ntitle Before: [use actual class name]\\nskinparam classAttributeIconSize 0\\n...\\n@enduml",
|
| 591 |
+
"after": "@startuml\\ntitle After: {context['pattern']} Pattern Applied\\nskinparam classAttributeIconSize 0\\n...\\n@enduml"
|
| 592 |
+
}}}}
|
| 593 |
+
|
| 594 |
+
DO NOT include ```json or ``` markers.
|
| 595 |
+
DO NOT add any explanation text.
|
| 596 |
+
OUTPUT ONLY THE JSON OBJECT.
|
| 597 |
"""
|
| 598 |
|
| 599 |
messages = [
|
| 600 |
+
SystemMessage(content="You are a UML expert. Analyze actual code and generate precise diagrams using real class/method names. Output only valid JSON, no markdown."),
|
| 601 |
HumanMessage(content=prompt)
|
| 602 |
]
|
| 603 |
|
| 604 |
+
logger.info(f"🤖 Sending code to AI for analysis...")
|
| 605 |
response = self.llm.invoke(messages)
|
| 606 |
content = response.content.strip()
|
| 607 |
|
| 608 |
+
# Clean up response - remove markdown if present
|
| 609 |
if "```json" in content:
|
| 610 |
content = content.split("```json")[1].split("```")[0].strip()
|
| 611 |
elif "```" in content:
|
| 612 |
content = content.split("```")[1].split("```")[0].strip()
|
| 613 |
|
| 614 |
+
# Find JSON object boundaries
|
| 615 |
+
start = content.find("{")
|
| 616 |
+
end = content.rfind("}") + 1
|
| 617 |
+
if start >= 0 and end > start:
|
| 618 |
+
content = content[start:end]
|
| 619 |
+
|
| 620 |
+
# Parse JSON
|
| 621 |
result = json.loads(content)
|
| 622 |
+
before = result.get("before", "")
|
| 623 |
+
after = result.get("after", "")
|
| 624 |
+
|
| 625 |
+
# Validate UML syntax
|
| 626 |
+
if "@startuml" not in before or "@enduml" not in before:
|
| 627 |
+
logger.warning("❌ AI returned invalid BEFORE UML")
|
| 628 |
+
raise ValueError("Invalid BEFORE UML from AI")
|
| 629 |
+
|
| 630 |
+
if "@startuml" not in after or "@enduml" not in after:
|
| 631 |
+
logger.warning("❌ AI returned invalid AFTER UML")
|
| 632 |
+
raise ValueError("Invalid AFTER UML from AI")
|
| 633 |
|
| 634 |
+
logger.info(f"✅ AI successfully generated custom UML for {rec.pattern} pattern")
|
| 635 |
+
logger.info(f" BEFORE diagram: {len(before)} chars")
|
| 636 |
+
logger.info(f" AFTER diagram: {len(after)} chars")
|
| 637 |
+
|
| 638 |
+
return before, after
|
| 639 |
+
|
| 640 |
+
except json.JSONDecodeError as e:
|
| 641 |
+
logger.error(f"❌ Failed to parse AI response as JSON: {e}")
|
| 642 |
+
logger.error(f" Response preview: {content[:200] if 'content' in locals() else 'N/A'}")
|
| 643 |
+
return self._generate_structure_based_uml(rec, structure, code)
|
| 644 |
+
|
| 645 |
+
except Exception as e:
|
| 646 |
+
logger.error(f"❌ AI UML generation failed: {e}")
|
| 647 |
+
import traceback
|
| 648 |
+
logger.error(traceback.format_exc())
|
| 649 |
+
logger.info("⚠️ Falling back to structure-based generation")
|
| 650 |
+
return self._generate_structure_based_uml(rec, structure, code)
|
| 651 |
+
|
| 652 |
+
def _extract_relevant_code(self, class_name: str, code: str) -> str:
|
| 653 |
+
"""Extract the code section relevant to the class being analyzed"""
|
| 654 |
+
try:
|
| 655 |
+
tree = ast.parse(code)
|
| 656 |
+
for node in ast.walk(tree):
|
| 657 |
+
if isinstance(node, ast.ClassDef) and node.name == class_name:
|
| 658 |
+
lines = code.split("\n")
|
| 659 |
+
start = node.lineno - 1
|
| 660 |
+
# Get reasonable chunk of code (not too much)
|
| 661 |
+
end = node.end_lineno if hasattr(node, 'end_lineno') else start + 30
|
| 662 |
+
end = min(end, start + 40) # Max 40 lines
|
| 663 |
+
return "\n".join(lines[start:end])
|
| 664 |
except Exception as e:
|
| 665 |
+
logger.warning(f"Failed to extract class code: {e}")
|
| 666 |
+
|
| 667 |
+
# Fallback: search for class definition manually
|
| 668 |
+
lines = code.split("\n")
|
| 669 |
+
for i, line in enumerate(lines):
|
| 670 |
+
if f"class {class_name}" in line:
|
| 671 |
+
return "\n".join(lines[i:min(i+30, len(lines))])
|
| 672 |
+
|
| 673 |
+
# Last resort: return first chunk of code
|
| 674 |
+
return code[:800]
|
| 675 |
|
| 676 |
+
# Keep the existing _recommend_* methods unchanged
|
| 677 |
def _recommend_strategy(self, structure: List[Dict], code: str):
|
| 678 |
"""Recommend Strategy pattern for complex conditionals"""
|
|
|
|
| 679 |
for cls in structure:
|
| 680 |
if cls.get("type") == "module":
|
| 681 |
continue
|
| 682 |
|
|
|
|
| 683 |
conditional_count = code.count("if ") + code.count("elif ")
|
| 684 |
|
| 685 |
+
if conditional_count > 5:
|
| 686 |
self.recommendations.append(PatternRecommendation(
|
| 687 |
pattern="Strategy",
|
| 688 |
location=cls["name"],
|
|
|
|
| 698 |
if cls.get("type") == "module":
|
| 699 |
continue
|
| 700 |
|
|
|
|
| 701 |
if "(" in code and code.count("= ") > 3:
|
| 702 |
self.recommendations.append(PatternRecommendation(
|
| 703 |
pattern="Factory",
|
|
|
|
| 710 |
|
| 711 |
def _recommend_singleton(self, structure: List[Dict], code: str):
|
| 712 |
"""Recommend Singleton for shared resources"""
|
|
|
|
| 713 |
if "global " in code or code.count("_instance") > 0:
|
| 714 |
self.recommendations.append(PatternRecommendation(
|
| 715 |
pattern="Singleton",
|
|
|
|
| 722 |
|
| 723 |
def _recommend_observer(self, structure: List[Dict], code: str):
|
| 724 |
"""Recommend Observer for event handling"""
|
|
|
|
| 725 |
if "callback" in code.lower() or "notify" in code.lower():
|
| 726 |
self.recommendations.append(PatternRecommendation(
|
| 727 |
pattern="Observer",
|
|
|
|
| 732 |
implementation_hint="Create Subject class with attach/detach/notify methods"
|
| 733 |
))
|
| 734 |
|
|
|
|
| 735 |
class PatternEnricher:
|
| 736 |
"""
|
| 737 |
Uses AI to enrich pattern detections with:
|