File size: 17,568 Bytes
8edee29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
"""
agents/code_generator.py
------------------------
Code Generator Agent for AutoDevAgent.

Takes the plan produced by the Planning Agent and generates executable
code from it. This is a pure code-writing step β€” no execution happens here.

Design:
  - Uses Groq's primary model (Llama 3.1 70B) for code quality.
  - Receives the full plan as context so the LLM writes code
    that matches the reasoning, not just the task description.
  - For SQL: also receives the inferred schema so the generated
    query targets the exact tables and columns that exist in SQLite.
  - Strips markdown fences from the output β€” executors need raw code.
  - Appends each generated version to code_history for full traceability.

Usage:
    from agents.code_generator import CodeGeneratorAgent
    from pipeline.state import PipelineState, Language

    agent = CodeGeneratorAgent()
    state = PipelineState(task="reverse a string", language=Language.PYTHON)
    # state.plan must already be populated by PlanningAgent
    updated = agent.run(state)
    print(updated["generated_code"])
"""

import logging
from typing import Any

from langchain_groq import ChatGroq
from langchain_core.messages import SystemMessage, HumanMessage

from config import settings
from pipeline.state import (
    PipelineState,
    PipelineStatus,
    Language,
    PlanStep,
    SQLSchema,
    SelfReflection,
    ErrorClassification,
)

logger = logging.getLogger(__name__)


# ------------------------------------------------------------------ #
#  Prompts                                                             #
# ------------------------------------------------------------------ #

PYTHON_CODE_SYSTEM = """
You are an expert Python developer.
You will be given a task and a step-by-step plan.
Write clean, correct Python code that implements the plan exactly.

Rules:
- Output ONLY the raw Python code. No markdown, no explanation, no code fences.
- Follow the plan steps in order.
- Include a main guard (if __name__ == "__main__":) with 1-2 simple, realistic example calls.
- Add a brief docstring to every function.
- Use descriptive variable names.
- Handle obvious edge cases (empty input, None, etc.) INSIDE the function body.

CRITICAL β€” __main__ block rules:
- ONLY use valid, non-None real-world example inputs in the __main__ block.
- NEVER call the function with None, empty string "", [], or any invalid/edge-case value in __main__.
- The __main__ block is for demonstration only β€” show the function working correctly with typical inputs.
- WRONG:  print(are_anagrams(None, "listen"))   # crashes executor
- CORRECT: print(are_anagrams("listen", "silent"))  # works fine
""".strip()

SQL_CODE_SYSTEM = """
You are an expert SQL developer working with SQLite.
You will be given a task, a step-by-step plan, and the exact schema with tables and columns.

Write a single SQL query that solves the task using ONLY the tables and columns defined in the schema.

Rules:
- Output ONLY the raw SQL query. No markdown, no explanation, no code fences.
- Use only SQLite-compatible SQL syntax.
- Reference only the exact table and column names from the schema.
- End the query with a semicolon.
""".strip()

PYTHON_CODE_REGEN_SYSTEM = """
You are an expert Python developer doing a complete rewrite.
Previous attempts to fix this code have all failed. You must start from scratch with a fresh approach.

You will be given:
  - The task description
  - The step-by-step plan
  - A history of what was tried and what errors occurred

Your job is to write completely new code that avoids ALL the errors listed in the history.
Do NOT patch or extend the previous code β€” write a different implementation from scratch.

Rules:
- Output ONLY the raw Python code. No markdown, no explanation, no code fences.
- Choose a different algorithm or implementation strategy than what was tried before.
- Include a main guard (if __name__ == "__main__":) with 1-2 simple, realistic example calls.
- ONLY use valid, non-None, real-world inputs in the __main__ block.
- Handle all edge cases mentioned in the error history INSIDE the function body.
- Add a brief docstring to every function.
""".strip()

SQL_CODE_REGEN_SYSTEM = """
You are an expert SQL developer doing a complete rewrite.
Previous attempts to fix this query have all failed. You must start from scratch with a fresh approach.

You will be given:
  - The task description
  - The schema (CREATE TABLE + INSERT statements)
  - A history of what was tried and what errors occurred

Your job is to write a completely new SQL query that avoids ALL the errors listed in the history.
Do NOT patch the previous query β€” write a different approach from scratch.

Rules:
- Output ONLY the raw SQL query. No markdown, no explanation, no code fences.
- Use only SQLite-compatible SQL syntax.
- Reference only the exact table and column names from the schema.
- End the query with a semicolon.
""".strip()


# ------------------------------------------------------------------ #
#  Agent                                                               #
# ------------------------------------------------------------------ #

class CodeGeneratorAgent:
    """
    Generates executable code from a structured plan.

    Reads the plan (and SQL schema if applicable) from state and
    produces clean, runnable code. Appends the result to code_history
    so every version is traceable across debug iterations.

    Attributes:
        llm: ChatGroq instance using the primary model (70B).
    """

    def __init__(self) -> None:
        """LLM client is built lazily in run() once model assignments are known."""
        self.llm = None

    def _build_llm(self, model: str) -> "ChatGroq":
        return ChatGroq(
            api_key=settings.groq_api_key,
            model=model,
            temperature=0.2,
            max_tokens=2000,
            request_timeout=settings.groq_request_timeout,
        )

    def run(self, state: PipelineState) -> dict[str, Any]:
        """
        Generate code from the plan in the current pipeline state.

        Builds a human message containing the task, the full plan,
        and (for SQL) the schema. Sends to the LLM, strips any
        markdown fences from the response, and returns the clean code.

        Args:
            state: Current PipelineState. Reads: task, language,
                   plan, sql_schema.

        Returns:
            Partial state dict with keys:
              - "generated_code": str  β€” raw, executable code
              - "code_history":   list β€” all versions including this one
              - "status":         PipelineStatus.GENERATING
        """
        ma = state.model_assignments or {}
        model = ma.get("generator", settings.groq_model_primary)
        self.llm = self._build_llm(model)
        logger.info("CodeGeneratorAgent using model: %s", model)

        logger.info(
            "CodeGeneratorAgent running (iteration %d)", state.debug_iterations
        )

        system_prompt = (
            SQL_CODE_SYSTEM
            if state.language == Language.SQL
            else PYTHON_CODE_SYSTEM
        )

        human_content = _build_human_message(
            task=state.task,
            plan=state.plan,
            language=state.language,
            sql_schema=state.sql_schema,
        )

        messages = [
            SystemMessage(content=system_prompt),
            HumanMessage(content=human_content),
        ]

        # ── LLM call ──────────────────────────────────────────────── #
        try:
            response = self.llm.invoke(messages)
            raw = response.content.strip()
            logger.debug("CodeGeneratorAgent raw response length: %d chars", len(raw))
        except Exception as e:
            logger.error("CodeGeneratorAgent LLM call failed: %s", e)
            raise RuntimeError(f"Code generator LLM call failed: {e}") from e

        # ── Track token usage ─────────────────────────────────────── #
        from observability.langsmith_tracer import extract_token_usage_from_response
        prompt_t, completion_t = extract_token_usage_from_response(response)
        updated_token_usage = state.token_usage.model_copy()
        updated_token_usage.add(prompt_t, completion_t)

        # ── Strip markdown fences ─────────────────────────────────── #
        code = _strip_code_fences(raw)

        if not code.strip():
            raise ValueError("CodeGeneratorAgent returned empty code.")

        logger.info(
            "CodeGeneratorAgent produced %d lines of %s code",
            len(code.splitlines()),
            state.language.value,
        )

        # ── Append to history ─────────────────────────────────────── #
        # Preserve all previous versions for traceability
        updated_history = list(state.code_history) + [code]

        return {
            "generated_code": code,
            "code_history":   updated_history,
            "status":         PipelineStatus.GENERATING,
            "token_usage":    updated_token_usage,
        }


    def regen_run(self, state: PipelineState) -> dict[str, Any]:
        """
        Fresh regeneration run β€” called when the debug loop has exhausted
        all retries without fixing the code.

        Unlike run(), which generates from the plan alone, regen_run()
        passes the full error history to the LLM and asks it to write a
        completely different implementation that avoids all known failures.

        Args:
            state: Current PipelineState. Reads: task, language, plan,
                   sql_schema, reflections_history, error_classification.

        Returns:
            Partial state dict with keys:
              - "generated_code": str  β€” fresh code, different approach
              - "code_history":   list β€” all versions including this one
              - "status":         PipelineStatus.REGENERATING
        """
        ma = state.model_assignments or {}
        model = ma.get("generator", settings.groq_model_primary)
        self.llm = self._build_llm(model)
        logger.info(
            "CodeGeneratorAgent.regen_run β€” regen #%d, model: %s",
            state.regen_count + 1, model,
        )

        system_prompt = (
            SQL_CODE_REGEN_SYSTEM
            if state.language == Language.SQL
            else PYTHON_CODE_REGEN_SYSTEM
        )

        human_content = _build_regen_human_message(
            task=state.task,
            plan=state.plan,
            language=state.language,
            sql_schema=state.sql_schema,
            reflections_history=state.reflections_history,
            error_classification=state.error_classification,
        )

        messages = [
            SystemMessage(content=system_prompt),
            HumanMessage(content=human_content),
        ]

        try:
            response = self.llm.invoke(messages)
            raw = response.content.strip()
        except Exception as e:
            logger.error("CodeGeneratorAgent.regen_run LLM call failed: %s", e)
            raise RuntimeError(f"Code regeneration LLM call failed: {e}") from e

        from observability.langsmith_tracer import extract_token_usage_from_response
        prompt_t, completion_t = extract_token_usage_from_response(response)
        updated_token_usage = state.token_usage.model_copy()
        updated_token_usage.add(prompt_t, completion_t)

        code = _strip_code_fences(raw)
        if not code.strip():
            raise ValueError("CodeGeneratorAgent.regen_run returned empty code.")

        logger.info(
            "CodeGeneratorAgent.regen_run produced %d lines of %s code",
            len(code.splitlines()), state.language.value,
        )

        updated_history = list(state.code_history) + [code]

        return {
            "generated_code": code,
            "code_history":   updated_history,
            "status":         PipelineStatus.REGENERATING,
            "token_usage":    updated_token_usage,
        }


# ------------------------------------------------------------------ #
#  Helpers                                                             #
# ------------------------------------------------------------------ #

def _build_human_message(
    task: str,
    plan: list[PlanStep],
    language: Language,
    sql_schema: SQLSchema | None,
) -> str:
    """
    Construct the human message sent to the LLM.

    Combines the task description, formatted plan steps, and (for SQL)
    the full schema into a single prompt string.

    Args:
        task:       The user's original task description.
        plan:       List of PlanStep objects from the Planning Agent.
        language:   The target language (PYTHON or SQL).
        sql_schema: Inferred schema for SQL tasks, else None.

    Returns:
        Formatted string ready to send as a HumanMessage.
    """
    lines: list[str] = [f"Task: {task}", "", "Plan:"]

    for step in plan:
        lines.append(f"  Step {step.step_number}: {step.description}")
        lines.append(f"    Reason: {step.reasoning}")

    # Append schema context for SQL tasks so the LLM uses correct table names
    if language == Language.SQL and sql_schema:
        lines.append("")
        lines.append("Database schema (SQLite):")
        for stmt in sql_schema.create_statements:
            lines.append(f"  {stmt}")
        if sql_schema.table_descriptions:
            lines.append("")
            lines.append("Table descriptions:")
            for desc in sql_schema.table_descriptions:
                lines.append(f"  - {desc}")

    lines.append("")
    lines.append("Write the code now:")
    return "\n".join(lines)


def _build_regen_human_message(
    task: str,
    plan: list[PlanStep],
    language: Language,
    sql_schema: SQLSchema | None,
    reflections_history: list,
    error_classification,
) -> str:
    """
    Build the human message for a fresh regeneration call.

    Includes the task, plan, schema (SQL), and the full history of
    what was tried and what went wrong so the LLM can deliberately
    avoid repeating the same mistakes.

    Args:
        task:                 The user's original task description.
        plan:                 Plan steps from PlanningAgent.
        language:             PYTHON or SQL.
        sql_schema:           Schema for SQL tasks, else None.
        reflections_history:  All SelfReflection objects across all debug iterations.
        error_classification: The last ErrorClassification (may be None).

    Returns:
        Formatted string ready to send as a HumanMessage.
    """
    lines: list[str] = [f"Task: {task}", "", "Plan:"]
    for step in plan:
        lines.append(f"  Step {step.step_number}: {step.description}")
        lines.append(f"    Reason: {step.reasoning}")

    # Schema for SQL
    if language == Language.SQL and sql_schema:
        lines.append("")
        lines.append("Database schema (SQLite):")
        for stmt in sql_schema.create_statements:
            lines.append(f"  {stmt}")
        lines.append("")
        lines.append("Dummy data:")
        for stmt in sql_schema.insert_statements:
            lines.append(f"  {stmt}")

    # Error history β€” most important context
    lines.append("")
    lines.append("Previous attempts all failed. Here is what was tried and what went wrong:")
    lines.append("(DO NOT repeat any of these approaches β€” use a completely different strategy)")

    if reflections_history:
        for i, r in enumerate(reflections_history, 1):
            lines.append(f"\n  Attempt {i}:")
            lines.append(f"    Error observed:  {r.what_i_saw}")
            lines.append(f"    Root cause:      {r.what_i_think}")
            lines.append(f"    Fix attempted:   {r.what_i_will_do}")
    else:
        lines.append("  (no reflection history available β€” write a robust implementation from scratch)")

    if error_classification:
        lines.append("")
        lines.append(f"Last error type: {error_classification.error_type}")
        lines.append(f"Last error hint: {error_classification.suggested_fix}")

    lines.append("")
    lines.append("Write completely new code now (different approach, no patches):")
    return "\n".join(lines)


def _strip_code_fences(raw: str) -> str:
    """
    Remove markdown code fences from LLM output.

    LLMs frequently wrap code in triple backtick fences (```python ...```)
    even when the prompt says not to. Raw executors need clean code.

    Handles these patterns:
      - ```python\\n...\\n```
      - ```sql\\n...\\n```
      - ```\\n...\\n```
      - No fences at all (returned as-is)

    Args:
        raw: Raw string from the LLM response.

    Returns:
        Clean code string with fences and language tags removed.
    """
    stripped = raw.strip()

    # Check if the response is wrapped in code fences
    if stripped.startswith("```"):
        lines = stripped.splitlines()
        # Remove the opening fence line (```python, ```sql, or just ```)
        start = 1
        # Remove the closing fence if present
        end = len(lines)
        if lines[-1].strip() == "```":
            end = len(lines) - 1
        return "\n".join(lines[start:end]).strip()

    return stripped