| """ |
| Generator node β produces initial and repaired Python code solutions. |
| |
| ROLE IN THE GRAPH |
| ----------------- |
| generate_solution runs TWICE in the graph lifecycle: |
| 1. At graph start (iteration=0): generates the INITIAL solution from scratch, |
| using only the task description and any cross-session lessons. |
| 2. In the repair loop (iteration>0): generates a TARGETED REPAIR guided by |
| the debugger's root_cause + repair_strategy from the previous iteration. |
| |
| This node uses the ``generator`` role, which is intentionally routed to a WEAK |
| model (Llama-3.2-3B via HuggingFace or Ollama). A weak generator is what makes |
| the self-healing loop interesting β it fails on edge cases reliably, triggering |
| diagnose β repair cycles that showcase the agent's autonomous correction ability. |
| All other roles (QA, debugger, critic) use Claude (strong model). |
| |
| TEMPLATE SELECTION |
| ------------------ |
| Two prompt templates live in ``prompts/generator.yaml``: |
| - ``initial``: "Write a Python function for: {task_description}" |
| + optional cross-session lessons as context. |
| - ``repair``: "Fix this code: {current_code} β failure: {test_results}, |
| root cause: {root_cause}, strategy: {repair_strategy}" |
| |
| WHY CROSS-SESSION LESSONS ON ITERATION 0? |
| ------------------------------------------ |
| If lesson_store is provided and iteration==0, we query it for the top-3 |
| lessons most similar to the current task (via embedding similarity). These |
| lessons come from PREVIOUS runs on similar tasks β e.g. "always handle empty |
| list input" or "remember to return None instead of raising on missing key". |
| Prepending them to the learning_log gives the generator a head-start so it |
| avoids common mistakes it (or the system) has seen before. |
| """ |
|
|
| import logging |
| from typing import TYPE_CHECKING, Any |
|
|
| from agent.state import AgentState |
| from agent.events import ( |
| step_event, |
| code_generated_event, |
| ) |
| from llm.router import LLMRouter |
|
|
| if TYPE_CHECKING: |
| from agent.memory_store import LessonStore |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| async def generate_solution( |
| state: AgentState, |
| router: LLMRouter, |
| lesson_store: "LessonStore | None" = None, |
| ) -> dict[str, Any]: |
| """LangGraph node: generate or repair a Python code solution. |
| |
| On iteration 0 β calls ``initial`` template: task description + cross-session lessons. |
| On iteration N>0 β calls ``repair`` template: task + diagnosis context + lessons. |
| |
| Args: |
| state: Current AgentState. Reads: iteration, task_description, |
| current_code, root_cause, repair_strategy, learning_log. |
| router: LLMRouter (bound via functools.partial in build_graph). |
| The generator role uses HF/Ollama; other roles use Claude. |
| lesson_store: Optional ChromaDB lesson store for cross-session retrieval. |
| Only consulted on iteration=0. |
| |
| Returns: |
| Partial state update: {"current_code": ..., "events": ...} |
| """ |
| iteration = state.get("iteration", 0) |
| |
| |
| events = list(state.get("events", [])) |
|
|
| |
| |
| events.append(step_event( |
| f"{'Generating initial solution' if iteration == 0 else 'Applying repair'}...", |
| iteration=iteration, |
| ).to_dict()) |
|
|
| |
| |
| |
| is_repair = ( |
| iteration > 0 |
| and state.get("current_code", "") |
| and state.get("root_cause", "") |
| ) |
|
|
| |
| |
| |
| |
| current_lessons = list(state.get("learning_log", [])) |
| if iteration == 0 and lesson_store is not None: |
| retrieved = lesson_store.retrieve_relevant_lessons( |
| query=state["task_description"], |
| n_results=3, |
| ) |
| if retrieved: |
| logger.info( |
| "Cross-session memory: prepending %d retrieved lessons", len(retrieved) |
| ) |
| |
| |
| current_lessons = retrieved + current_lessons |
|
|
| |
| learning_log = _format_learning_log(current_lessons) |
|
|
| |
| if is_repair: |
| |
| |
| |
| |
| template_key = "repair" |
| variables = { |
| "task_description": state["task_description"], |
| "current_code": state["current_code"], |
| "test_results": state.get("last_failure_summary", "No failure details."), |
| "root_cause": state.get("root_cause", "Unknown"), |
| "repair_strategy": state.get("repair_strategy", "No strategy available."), |
| "learning_log": learning_log, |
| } |
| else: |
| |
| template_key = "initial" |
| variables = { |
| "task_description": state["task_description"], |
| "learning_log": learning_log, |
| } |
|
|
| |
| |
| |
| |
| result = await router.call( |
| role="generator", |
| template_key=template_key, |
| variables=variables, |
| max_new_tokens=2048, |
| ) |
|
|
| code = result["code"] |
| explanation = result.get("explanation", "") |
|
|
| logger.info("Generator produced %d chars of code (iteration=%d)", len(code), iteration) |
|
|
| |
| |
| events.append(code_generated_event( |
| code=code, |
| iteration=iteration, |
| explanation=explanation, |
| ).to_dict()) |
|
|
| |
| |
| return { |
| "current_code": code, |
| "events": events, |
| } |
|
|
|
|
| def _format_learning_log(lessons: list[str]) -> str: |
| """Convert a list of lesson strings into a bullet-list for prompt injection. |
| |
| Returns a placeholder string if empty, so the prompt template always has |
| something to render at the {learning_log} variable slot. |
| """ |
| if not lessons: |
| return "No prior lessons recorded." |
| return "\n".join(f"- {lesson}" for lesson in lessons) |
|
|