mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
feat: enhance component migration orchestrator with conflict resolution- Add conflict resolution agent to handle unified diffs.- Refactor existing file reading logic to improve clarity.- Update component target categorization to use root name.- Introduce new prompt templates for conflict resolution and analysis.- Improve diff application logic for better error handling and performance.
This commit is contained in:
@@ -8,10 +8,10 @@ Usage:
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
import difflib
|
||||
import re
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
@@ -40,17 +40,6 @@ class ComponentTarget:
|
||||
category: str
|
||||
|
||||
|
||||
ANALYSIS_AGENT = Agent(
|
||||
name="ComponentAnalyzer",
|
||||
instructions=(
|
||||
"You are a migration assistant. Given a TSX component, analyze it and "
|
||||
"produce a JSON-only response that includes: stateful analysis, "
|
||||
"hook details if needed, JSON UI definition, TS interface, "
|
||||
"and export updates. Follow the provided workflow."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
PROMPT_TEMPLATE = """\
|
||||
You are converting a React TSX component to the JSON-driven system.
|
||||
Follow this workflow strictly:
|
||||
@@ -71,7 +60,7 @@ IMPORTANT:
|
||||
- For json-components.ts, output ONLY the snippet for the component export, matching this style:
|
||||
export const ComponentName = createJsonComponent<ComponentNameProps>(componentNameDef)
|
||||
OR
|
||||
export const ComponentName = createJsonComponentWithHooks<ComponentNameProps>(componentNameDef, { ... })
|
||||
export const ComponentName = createJsonComponentWithHooks<ComponentNameProps>(componentNameDef, {{ ... }})
|
||||
- Do NOT output any `jsonComponents` object or registry literal.
|
||||
- Provide `diffs` with unified diff lines (one string per line) for each target file,
|
||||
using the provided file contents as the "before" version.
|
||||
@@ -124,6 +113,27 @@ TSX source:
|
||||
"""
|
||||
|
||||
|
||||
CONFLICT_PROMPT_TEMPLATE = """\
|
||||
You are resolving a unified diff that failed to apply cleanly.
|
||||
Return ONLY valid JSON with this shape:
|
||||
{{
|
||||
"path": "{path}",
|
||||
"resolvedContent": "...full file content..."
|
||||
}}
|
||||
|
||||
Rules:
|
||||
- Use the diff to update the original content.
|
||||
- Preserve unrelated content.
|
||||
- Do NOT return Markdown or code fences.
|
||||
|
||||
Original content:
|
||||
{original}
|
||||
|
||||
Diff lines:
|
||||
{diff_lines}
|
||||
"""
|
||||
|
||||
|
||||
def list_components(roots: Iterable[Path]) -> List[ComponentTarget]:
|
||||
targets: List[ComponentTarget] = []
|
||||
for root in roots:
|
||||
@@ -132,7 +142,7 @@ def list_components(roots: Iterable[Path]) -> List[ComponentTarget]:
|
||||
for path in sorted(root.rglob("*.tsx")):
|
||||
name = path.stem
|
||||
targets.append(
|
||||
ComponentTarget(name=name, path=path, category=path.parent.name)
|
||||
ComponentTarget(name=name, path=path, category=root.name)
|
||||
)
|
||||
return targets
|
||||
|
||||
@@ -161,21 +171,52 @@ def _read_file_for_prompt(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def run_agent_for_component(target: ComponentTarget, debug: bool = False) -> Dict[str, Any]:
|
||||
def _read_existing_file(out_dir: Path, rel_path: str) -> str:
|
||||
out_path = out_dir / rel_path
|
||||
if out_path.exists():
|
||||
return out_path.read_text(encoding="utf-8")
|
||||
return _read_file_for_prompt(ROOT / rel_path)
|
||||
|
||||
|
||||
def _build_agent() -> Agent:
|
||||
return Agent(
|
||||
name="ComponentAnalyzer",
|
||||
instructions=(
|
||||
"You are a migration assistant. Given a TSX component, analyze it and "
|
||||
"produce a JSON-only response that includes: stateful analysis, "
|
||||
"hook details if needed, JSON UI definition, TS interface, "
|
||||
"and export updates. Follow the provided workflow."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _build_conflict_agent() -> Agent:
|
||||
return Agent(
|
||||
name="DiffConflictResolver",
|
||||
instructions=(
|
||||
"Resolve unified diff conflicts by producing the fully merged file content. "
|
||||
"Return JSON only."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def run_agent_for_component(
|
||||
target: ComponentTarget, out_dir: Path, debug: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
tsx = target.path.read_text(encoding="utf-8")
|
||||
existing_files = {
|
||||
"src/lib/json-ui/json-components.ts": _read_file_for_prompt(
|
||||
ROOT / "src" / "lib" / "json-ui" / "json-components.ts"
|
||||
"src/lib/json-ui/json-components.ts": _read_existing_file(
|
||||
out_dir, "src/lib/json-ui/json-components.ts"
|
||||
),
|
||||
"src/lib/json-ui/interfaces/index.ts": _read_file_for_prompt(
|
||||
ROOT / "src" / "lib" / "json-ui" / "interfaces" / "index.ts"
|
||||
"src/lib/json-ui/interfaces/index.ts": _read_existing_file(
|
||||
out_dir, "src/lib/json-ui/interfaces/index.ts"
|
||||
),
|
||||
"src/hooks/index.ts": _read_file_for_prompt(ROOT / "src" / "hooks" / "index.ts"),
|
||||
"src/lib/json-ui/hooks-registry.ts": _read_file_for_prompt(
|
||||
ROOT / "src" / "lib" / "json-ui" / "hooks-registry.ts"
|
||||
"src/hooks/index.ts": _read_existing_file(out_dir, "src/hooks/index.ts"),
|
||||
"src/lib/json-ui/hooks-registry.ts": _read_existing_file(
|
||||
out_dir, "src/lib/json-ui/hooks-registry.ts"
|
||||
),
|
||||
f"src/components/{target.category}/index.ts": _read_file_for_prompt(
|
||||
ROOT / "src" / "components" / target.category / "index.ts"
|
||||
f"src/components/{target.category}/index.ts": _read_existing_file(
|
||||
out_dir, f"src/components/{target.category}/index.ts"
|
||||
),
|
||||
}
|
||||
existing_files_blob = "\n\n".join(
|
||||
@@ -187,7 +228,7 @@ def run_agent_for_component(target: ComponentTarget, debug: bool = False) -> Dic
|
||||
tsx=tsx,
|
||||
existing_files=existing_files_blob,
|
||||
)
|
||||
result = Runner.run_sync(ANALYSIS_AGENT, prompt)
|
||||
result = Runner.run_sync(_build_agent(), prompt)
|
||||
output = getattr(result, "final_output", None)
|
||||
if output is None:
|
||||
output = str(result)
|
||||
@@ -209,6 +250,38 @@ def run_agent_for_component(target: ComponentTarget, debug: bool = False) -> Dic
|
||||
return data
|
||||
|
||||
|
||||
def _resolve_diff_with_agent(
|
||||
path: str, original: str, diff_lines: List[str], debug: bool
|
||||
) -> str:
|
||||
diff_blob = "\n".join(diff_lines)
|
||||
prompt = CONFLICT_PROMPT_TEMPLATE.format(
|
||||
path=path,
|
||||
original=original,
|
||||
diff_lines=diff_blob,
|
||||
)
|
||||
result = Runner.run_sync(_build_conflict_agent(), prompt)
|
||||
output = getattr(result, "final_output", None)
|
||||
if output is None:
|
||||
output = str(result)
|
||||
if not isinstance(output, str) or not output.strip():
|
||||
raise ValueError("Conflict resolver returned empty output.")
|
||||
output = _extract_json_payload(output)
|
||||
if debug:
|
||||
preview = textwrap.shorten(output.replace("\n", " "), width=300, placeholder="...")
|
||||
print(f"[debug] conflict resolver output preview: {preview}")
|
||||
try:
|
||||
data = json.loads(output)
|
||||
except json.JSONDecodeError as exc:
|
||||
snippet = output.strip().replace("\n", " ")[:200]
|
||||
raise ValueError(
|
||||
f"Conflict resolver output was not valid JSON: {exc}. Output starts with: {snippet!r}"
|
||||
) from exc
|
||||
resolved = data.get("resolvedContent")
|
||||
if not isinstance(resolved, str) or not resolved.strip():
|
||||
raise ValueError("Conflict resolver did not return resolvedContent.")
|
||||
return resolved
|
||||
|
||||
|
||||
def _write_if_content(path: Path, content: str) -> None:
|
||||
if not content.strip():
|
||||
return
|
||||
@@ -245,30 +318,49 @@ def _apply_unified_diff(original: str, diff_lines: List[str]) -> str:
|
||||
lines = original.splitlines()
|
||||
out: List[str] = []
|
||||
i = 0
|
||||
line_iter = iter(diff_lines)
|
||||
for line in line_iter:
|
||||
idx = 0
|
||||
applied = False
|
||||
|
||||
header_re = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
|
||||
|
||||
while idx < len(diff_lines):
|
||||
line = diff_lines[idx]
|
||||
if line.startswith("---") or line.startswith("+++"):
|
||||
idx += 1
|
||||
continue
|
||||
if line.startswith("@@"):
|
||||
header = line
|
||||
try:
|
||||
_, ranges, _ = header.split("@@", 2)
|
||||
left, _right = ranges.strip().split(" ", 1)
|
||||
left_start = int(left.split(",")[0].lstrip("-")) - 1
|
||||
except Exception:
|
||||
raise ValueError(f"Invalid diff header: {header!r}")
|
||||
out.extend(lines[i:left_start])
|
||||
i = left_start
|
||||
continue
|
||||
if line.startswith(" "):
|
||||
out.append(line[1:])
|
||||
i += 1
|
||||
elif line.startswith("-"):
|
||||
i += 1
|
||||
elif line.startswith("+"):
|
||||
out.append(line[1:])
|
||||
else:
|
||||
if not line.startswith("@@"):
|
||||
idx += 1
|
||||
continue
|
||||
match = header_re.match(line)
|
||||
if not match:
|
||||
raise ValueError(f"Invalid diff header: {line!r}")
|
||||
old_start = int(match.group(1))
|
||||
old_start_index = max(old_start - 1, 0)
|
||||
out.extend(lines[i:old_start_index])
|
||||
i = old_start_index
|
||||
idx += 1
|
||||
applied = True
|
||||
while idx < len(diff_lines) and not diff_lines[idx].startswith("@@"):
|
||||
hunk_line = diff_lines[idx]
|
||||
if hunk_line.startswith(" "):
|
||||
expected = hunk_line[1:]
|
||||
if i >= len(lines) or lines[i] != expected:
|
||||
raise ValueError("Diff context mismatch while applying patch.")
|
||||
out.append(lines[i])
|
||||
i += 1
|
||||
elif hunk_line.startswith("-"):
|
||||
expected = hunk_line[1:]
|
||||
if i >= len(lines) or lines[i] != expected:
|
||||
raise ValueError("Diff delete mismatch while applying patch.")
|
||||
i += 1
|
||||
elif hunk_line.startswith("+"):
|
||||
out.append(hunk_line[1:])
|
||||
elif hunk_line.startswith("\\"):
|
||||
pass
|
||||
idx += 1
|
||||
|
||||
if not applied:
|
||||
raise ValueError("No diff hunks found to apply.")
|
||||
out.extend(lines[i:])
|
||||
return "\n".join(out) + ("\n" if original.endswith("\n") else "")
|
||||
|
||||
@@ -310,9 +402,20 @@ def write_output(out_dir: Path, data: Dict[str, Any], target: ComponentTarget) -
|
||||
diff_lines = diff_entry.get("diffLines") or []
|
||||
if not path_value or not isinstance(diff_lines, list):
|
||||
continue
|
||||
target_path = out_dir / path_value
|
||||
target_path = (out_dir / path_value).resolve()
|
||||
if not target_path.is_relative_to(out_dir.resolve()):
|
||||
raise ValueError(f"Diff path escapes output directory: {path_value}")
|
||||
existing = target_path.read_text(encoding="utf-8") if target_path.exists() else ""
|
||||
merged = _apply_unified_diff(existing, diff_lines)
|
||||
try:
|
||||
merged = _apply_unified_diff(existing, diff_lines)
|
||||
except ValueError as exc:
|
||||
print(
|
||||
f"[warn] diff apply failed for {path_value}: {exc}; attempting AI resolution.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
merged = _resolve_diff_with_agent(
|
||||
path_value, existing, diff_lines, debug=True
|
||||
)
|
||||
_write_if_content(target_path, merged)
|
||||
return
|
||||
|
||||
@@ -349,7 +452,9 @@ def main() -> int:
|
||||
|
||||
failures: List[str] = []
|
||||
with ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
futures = {executor.submit(run_agent_for_component, t, True): t for t in targets}
|
||||
futures = {
|
||||
executor.submit(run_agent_for_component, t, out_dir, True): t for t in targets
|
||||
}
|
||||
for future in as_completed(futures):
|
||||
target = futures[future]
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user