|
20 | 20 | from app.application.agents.conversion.conversion_agent import conversion_agent |
21 | 21 | from app.infrastructure.utils.Agent_helpers.conversion_helper import _conversion_event_helper |
22 | 22 | from app.infrastructure.utils.language_adapters import adapter_for_file, get_adapter, ExecutionContract |
| 23 | +from app.infrastructure.utils.conversion_recipe_engine import ( |
| 24 | + RecipeContext, |
| 25 | + build_conversion_rules, |
| 26 | + sanitize_generated_code, |
| 27 | + validate_target_fragment, |
| 28 | + format_validation_feedback, |
| 29 | +) |
23 | 30 | from app.domain.interfaces.i_folder_structure_goals_repository import ( |
24 | 31 | IFolderStructureGoalsRepository, |
25 | 32 | ) |
@@ -272,141 +279,135 @@ def _normalize_target_file_path(target_file: str, source_file_path: str, target_ |
272 | 279 |
|
273 | 280 |
|
274 | 281 | def generate_new_code(step_input: StepInput) -> dict: |
275 | | - """Fetches meta data for a given symbol by filtering KB""" |
276 | | - # print(step_input, "+++") |
277 | | - prev_output = step_input.get_step_content("get_source_code") |
278 | | - source_code = prev_output.get("raw_code") |
279 | | - source_file_path = prev_output.get("file_path") |
280 | | - symbol_id = prev_output.get("symbol_id") |
281 | | - symbol_hash = prev_output.get("symbol_hash") |
282 | | - workflow_input = step_input.input |
| 282 | + """Generate one planned target symbol with deterministic guardrails.""" |
| 283 | + prev_output = step_input.get_step_content("get_source_code") or {} |
| 284 | + source_code = prev_output.get("raw_code") or "" |
| 285 | + source_file_path = prev_output.get("file_path") or "" |
| 286 | + symbol_id = prev_output.get("symbol_id") or "" |
| 287 | + symbol_hash = prev_output.get("symbol_hash") or "" |
| 288 | + workflow_input = step_input.input or {} |
283 | 289 | runtime_tech = _get_runtime_tech_context() |
284 | | - target_file_path = _normalize_target_file_path( |
285 | | - workflow_input.get("target_file"), |
286 | | - source_file_path, |
287 | | - runtime_tech.get("language", ""), |
288 | | - ) |
289 | | - target_symbol_name = workflow_input.get("target_symbol_name") |
290 | | - dependencies_output = step_input.get_step_content("get_dependencies") |
291 | | - file_deps = dependencies_output.get("file_dependencies") |
292 | | - deps = dependencies_output.get("dependent_code_snippets") |
293 | | - file_deps_content = file_deps if isinstance(file_deps, str) else "\n".join(file_deps) |
294 | | - deps_content = "\n".join(deps) |
295 | | - tech_summary = ( |
296 | | - f"Target Language : {runtime_tech['language']}\n" |
297 | | - f"Framework : {runtime_tech['framework'] or 'None'}\n" |
298 | | - f"Architecture : {runtime_tech['architecture'] or 'Unknown'}\n" |
| 290 | + target_language = runtime_tech.get("language", "") or "" |
| 291 | + target_file_path = _normalize_target_file_path(workflow_input.get("target_file"), source_file_path, target_language) |
| 292 | + target_symbol_name = workflow_input.get("target_symbol_name") or "migrated_symbol" |
| 293 | + |
| 294 | + dependencies_output = step_input.get_step_content("get_dependencies") or {} |
| 295 | + file_deps = dependencies_output.get("file_dependencies") or [] |
| 296 | + deps = dependencies_output.get("dependent_code_snippets") or [] |
| 297 | + file_deps_content = file_deps if isinstance(file_deps, str) else "\n".join(str(x) for x in file_deps) |
| 298 | + deps_content = deps if isinstance(deps, str) else "\n".join(str(x) for x in deps) |
| 299 | + tech_summary = (f"Target Language : {target_language}\n" f"Framework : {runtime_tech.get('framework') or 'None'}\n" f"Architecture : {runtime_tech.get('architecture') or 'Unknown'}\n") |
| 300 | + |
| 301 | + recipe_context = RecipeContext( |
| 302 | + source_language=(Path(str(source_file_path)).suffix.lstrip(".") or "unknown"), |
| 303 | + target_language=target_language, |
| 304 | + target_framework=runtime_tech.get("framework", "") or "", |
| 305 | + target_architecture=runtime_tech.get("architecture", "") or "", |
299 | 306 | ) |
| 307 | + conversion_rules = build_conversion_rules(recipe_context) |
300 | 308 |
|
301 | | - # Resolve executable behavior through the language adapter registry. The |
302 | | - # conversion engine never branches on source/target language itself. |
303 | 309 | execution_contract = None |
304 | 310 | entrypoint_context = "" |
305 | 311 | try: |
306 | 312 | source_adapter = adapter_for_file(Path(str(source_file_path))) |
307 | 313 | if source_adapter is not None: |
308 | | - execution_contract = source_adapter.detect_execution_contract( |
309 | | - Path(str(source_file_path)), target_symbol_name |
310 | | - ) |
| 314 | + execution_contract = source_adapter.detect_execution_contract(Path(str(source_file_path)), target_symbol_name) |
311 | 315 | if execution_contract and execution_contract.executable: |
312 | | - target_adapter = get_adapter(runtime_tech.get("language", "")) |
313 | | - target_name = target_adapter.display_name if target_adapter else runtime_tech.get("language", "target") |
| 316 | + target_adapter = get_adapter(target_language) |
| 317 | + target_name = target_adapter.display_name if target_adapter else (target_language or "target") |
314 | 318 | entrypoint_context = ( |
315 | | - "SOURCE EXECUTION CONTRACT: the source module executes the translated " |
316 | | - f"symbol '{execution_contract.entry_symbol}'. Preserve that runtime behavior " |
317 | | - f"using the idiomatic executable entry-point convention of the target language ({target_name}). " |
318 | | - "Do not add an entry point when the source is clearly a reusable library/module." |
| 319 | + "SOURCE EXECUTION CONTRACT: preserve execution of " |
| 320 | + f"'{execution_contract.entry_symbol}' using the idiomatic entry-point convention " |
| 321 | + f"of {target_name}. Do not add an entry point for a reusable library/module." |
319 | 322 | ) |
320 | 323 | except Exception: |
321 | | - execution_contract = None |
322 | | - entrypoint_context = "" |
| 324 | + logger.exception("Failed to determine source execution contract for %s", source_file_path) |
323 | 325 |
|
324 | 326 | try: |
325 | 327 | user = current_user.get() |
326 | 328 | migration_name = migration_name_ctx.get(None) |
327 | | - if migration_name not in _conversion_step_start_sent: |
328 | | - _conversion_step_start_sent.add(migration_name) |
329 | | - _conversion_event_helper.send_step_start( |
330 | | - AgentEventMessages.MIGRATION_PROGRESS_STEP_ID, |
331 | | - AgentEventMessages.MIGRATION_PROGRESS_STEP_NAME, |
332 | | - user, |
333 | | - MigrationEvent.MIGRATION_PROGRESS, |
334 | | - ) |
335 | | - _conversion_event_helper.send_step_description( |
336 | | - AgentEventMessages.MIGRATION_PROGRESS_STEP_ID, |
337 | | - AgentEventMessages.MIGRATION_PROGRESS_STEP_NAME, |
338 | | - user, |
339 | | - MigrationEvent.MIGRATION_PROGRESS, |
340 | | - ) |
341 | | - _conversion_event_helper.send_step_log( |
342 | | - AgentEventMessages.MIGRATION_PROGRESS_STEP_ID, |
343 | | - f"Generating converted code for: {target_symbol_name} → {target_file_path}", |
344 | | - user, |
345 | | - MigrationEvent.MIGRATION_PROGRESS, |
346 | | - ) |
| 329 | + step_key = f"{getattr(user, 'id', 'unknown')}:{migration_name}" |
| 330 | + if step_key not in _conversion_step_start_sent: |
| 331 | + _conversion_step_start_sent.add(step_key) |
| 332 | + _conversion_event_helper.send_step_start(AgentEventMessages.MIGRATION_PROGRESS_STEP_ID, AgentEventMessages.MIGRATION_PROGRESS_STEP_NAME, user, MigrationEvent.MIGRATION_PROGRESS) |
| 333 | + _conversion_event_helper.send_step_description(AgentEventMessages.MIGRATION_PROGRESS_STEP_ID, AgentEventMessages.MIGRATION_PROGRESS_STEP_NAME, user, MigrationEvent.MIGRATION_PROGRESS) |
| 334 | + _conversion_event_helper.send_step_log(AgentEventMessages.MIGRATION_PROGRESS_STEP_ID, f"Generating converted code for: {target_symbol_name} → {target_file_path}", user, MigrationEvent.MIGRATION_PROGRESS) |
347 | 335 | except Exception: |
348 | 336 | pass |
349 | 337 |
|
350 | | - resp = conversion_agent.run(input=f""" |
351 | | - Convert the given source symbol into equivalent target language code while making |
352 | | - sure any dependencies are resolved/imported correctly and any similar |
353 | | - code snippets (if given) are used as coding/formatting guidelines. |
354 | | - SOURCE CODE: {source_code} |
355 | | - SYMBOL ID : {symbol_id} |
356 | | - SYMBOL HASH : {symbol_hash} |
357 | | - SOURCE SYMBOL FILE PATH : {source_file_path} |
358 | | - TARGET TECH SUMMARY: {tech_summary} |
359 | | - EXISTING DEPENDENCIES TO USE: {deps_content} |
360 | | - EXISTING SIMILAR CODE SNIPPETS: [] |
361 | | - PRE-APPROVED LIBRARIES (from dependency file — use these for imports, do not invent others): {file_deps_content} |
362 | | - {entrypoint_context} |
363 | | - When writing the target code, make sure to correctly use the exisisting dependencies alongwith their correct import paths/naming conventions. |
364 | | - Return just the final raw code, no extra explanations & no extra formatting such as ```python, ```, etc. |
365 | | - """ |
366 | | - ) |
| 338 | + prompt = f""" |
| 339 | +Convert the given source symbol into equivalent target-language code while resolving dependencies and preserving externally observable behavior. |
367 | 340 |
|
368 | | - raw_response = (resp.content or "").strip() |
369 | | - target_code = _extract_clean_code(raw_response) |
| 341 | +{conversion_rules} |
| 342 | +
|
| 343 | +SOURCE CODE: |
| 344 | +{source_code} |
| 345 | +
|
| 346 | +SYMBOL ID: {symbol_id} |
| 347 | +SYMBOL HASH: {symbol_hash} |
| 348 | +SOURCE SYMBOL FILE PATH: {source_file_path} |
| 349 | +TARGET FILE PATH: {target_file_path} |
| 350 | +TARGET SYMBOL NAME: {target_symbol_name} |
| 351 | +
|
| 352 | +DEPENDENT CONVERTED CODE: |
| 353 | +{deps_content or 'None'} |
| 354 | +
|
| 355 | +PRE-APPROVED LIBRARIES (use these for imports; do not invent replacements): |
| 356 | +{file_deps_content or 'None'} |
| 357 | +
|
| 358 | +{entrypoint_context} |
| 359 | +
|
| 360 | +Return ONLY the final raw target code. Do not include markdown fences, explanations, placeholders, TODOs, or pseudocode. |
| 361 | +""" |
| 362 | + resp = conversion_agent.run(input=prompt) |
| 363 | + raw_response = (getattr(resp, "content", "") or "").strip() |
| 364 | + target_code = sanitize_generated_code(_extract_clean_code(raw_response)) |
370 | 365 | lowered = target_code.strip().lower() |
371 | | - if ( |
372 | | - not target_code.strip() |
373 | | - or lowered in {"unknown model error", "model error", "rate limit error", "rate limited"} |
374 | | - or "rate limit reached" in lowered |
375 | | - or "too many requests" in lowered |
376 | | - ): |
377 | | - raise RuntimeError( |
378 | | - "LLM code generation failed before producing source code. " |
379 | | - "The LLM gateway may be rate-limited; retry after the gateway cooldown." |
380 | | - ) |
381 | | - if target_code.startswith("```"): |
382 | | - target_code = target_code.split("\n", 1)[1] if "\n" in target_code else "" |
383 | | - if target_code.endswith("```"): |
384 | | - target_code = target_code.rsplit("\n", 1)[0] if "\n" in target_code else target_code[:-3] |
| 366 | + if (not target_code.strip() or lowered in {"unknown model error", "model error", "rate limit error", "rate limited"} or "rate limit reached" in lowered or "too many requests" in lowered): |
| 367 | + raise RuntimeError("LLM code generation failed before producing source code. The LLM gateway may be rate-limited; retry after the gateway cooldown.") |
385 | 368 |
|
386 | | - target_adapter = get_adapter(runtime_tech.get("language", "")) |
387 | | - if target_adapter is not None and execution_contract is not None: |
388 | | - target_code = target_adapter.ensure_entrypoint( |
389 | | - target_code, execution_contract, target_symbol_name |
390 | | - ) |
| 369 | + fragment_validation = validate_target_fragment(target_code, target_language) |
| 370 | + repair_attempts = max(0, int(os.getenv("CONVERSION_FRAGMENT_REPAIR_ATTEMPTS", "1"))) |
| 371 | + for _ in range(repair_attempts): |
| 372 | + if fragment_validation.valid or not fragment_validation.available: |
| 373 | + break |
| 374 | + repair_prompt = f""" |
| 375 | +Repair ONLY the generated target-code fragment below. Do not translate it again from scratch. Preserve its intended behavior and public interface. |
| 376 | +Return only corrected raw target code. |
391 | 377 |
|
| 378 | +{conversion_rules} |
| 379 | +
|
| 380 | +VALIDATION FEEDBACK: |
| 381 | +{format_validation_feedback(fragment_validation)} |
| 382 | +
|
| 383 | +SOURCE CODE: |
| 384 | +{source_code} |
| 385 | +
|
| 386 | +CURRENT TARGET CODE: |
| 387 | +{target_code} |
| 388 | +
|
| 389 | +PRE-APPROVED LIBRARIES: |
| 390 | +{file_deps_content or 'None'} |
| 391 | +""" |
| 392 | + repair_resp = conversion_agent.run(input=repair_prompt) |
| 393 | + target_code = sanitize_generated_code(_extract_clean_code((getattr(repair_resp, "content", "") or "").strip())) |
| 394 | + track_tokens(repair_resp, source="conversion:fragment_repair") |
| 395 | + fragment_validation = validate_target_fragment(target_code, target_language) |
| 396 | + if not fragment_validation.valid and fragment_validation.available: |
| 397 | + raise RuntimeError("Generated target-code fragment failed syntax validation after " + f"{repair_attempts} repair attempt(s): {format_validation_feedback(fragment_validation)}") |
| 398 | + |
| 399 | + target_adapter = get_adapter(target_language) |
| 400 | + if target_adapter is not None and execution_contract is not None: |
| 401 | + target_code = target_adapter.ensure_entrypoint(target_code, execution_contract, target_symbol_name) |
392 | 402 | track_tokens(resp, source="conversion:symbol_convert") |
393 | 403 |
|
394 | 404 | try: |
395 | 405 | user = current_user.get() |
396 | | - _conversion_event_helper.send_step_result( |
397 | | - AgentEventMessages.MIGRATION_PROGRESS_STEP_ID, |
398 | | - AgentEventMessages.MIGRATION_PROGRESS_STEP_NAME, |
399 | | - f"Code generated for {target_symbol_name}", |
400 | | - user, |
401 | | - MigrationEvent.MIGRATION_PROGRESS, |
402 | | - ) |
| 406 | + _conversion_event_helper.send_step_result(AgentEventMessages.MIGRATION_PROGRESS_STEP_ID, AgentEventMessages.MIGRATION_PROGRESS_STEP_NAME, f"Code generated for {target_symbol_name}", user, MigrationEvent.MIGRATION_PROGRESS) |
403 | 407 | except Exception: |
404 | 408 | pass |
405 | 409 | _send_plan_step_progress(4, workflow_input.get("plan_id", "")) |
406 | | - return StepOutput(content={ |
407 | | - "target_code": target_code, |
408 | | - "symbol_hash": symbol_hash, |
409 | | - }) |
| 410 | + return StepOutput(content={"target_code": target_code, "symbol_hash": symbol_hash, "fragment_validation": {"valid": fragment_validation.valid, "available": fragment_validation.available, "validator": fragment_validation.validator, "diagnostics": list(fragment_validation.diagnostics)}}) |
410 | 411 |
|
411 | 412 | def save_code_to_kb(step_input: StepInput) -> StepOutput: |
412 | 413 | """Save converted code to file and KB, track part-wise completion.""" |
|
0 commit comments