Files
task-6a22c713fd30e81cf315ea04/main.py
T

170 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import asyncio
from typing import TypedDict, Annotated, Dict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
from deepagents import create_deep_agent
from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend
from deepagents.tools import tool
# ---------- LLM ----------
llm = ChatOpenAI(
model="openai/gpt-oss-20b:free",
base_url="https://openrouter.ai/api/v1",
api_key=os.getenv("OPENAI_API_KEY"),
temperature=0.0,
)
# ---------- State ----------
class CodeReviewState(TypedDict):
code: str
draft_review: str
criteria_scores: Dict[str, int]
weakest_criterion: str
verdict: str
round: int
max_rounds: int
# ---------- Structured output for reflect ----------
class ReflectOutput(BaseModel):
pep8: int = Field(..., ge=0, le=10)
type_hints: int = Field(..., ge=0, le=10)
edge_cases: int = Field(..., ge=0, le=10)
naming: int = Field(..., ge=0, le=10)
weakest_criterion: str = Field(...)
verdict: str = Field(..., regex="^(ok|needs_revision)$")
reflect_parser = PydanticOutputParser(pydantic_object=ReflectOutput)
# ---------- Nodes ----------
@tool
def draft_review(state: CodeReviewState) -> CodeReviewState:
"""Generate an initial code review."""
prompt = (
"You are a senior Python reviewer.\n"
"Given the following function, write a concise code review (36 points).\n"
"Focus on style, correctness, and potential improvements.\n"
"Return only the review text.\n\n"
f"Function:\n{state['code']}"
)
review = llm.invoke([HumanMessage(content=prompt)]).content
state['draft_review'] = review
return state
@tool
def reflect(state: CodeReviewState) -> CodeReviewState:
"""Critique the draft review and score four criteria."""
prompt = (
"You are an automated code review critic.\n"
"Given the original code and the draft review, assign a score 010 for each of the following criteria:\n"
"- pep8: adherence to PEP8 style guide\n"
"- type_hints: use of type hints\n"
"- edge_cases: handling of edge cases\n"
"- naming: clarity of identifiers\n"
"Also identify the weakest criterion and decide if the review is "ok" or "needs_revision".\n"
"Return a JSON object with keys: pep8, type_hints, edge_cases, naming, weakest_criterion, verdict.\n"
"Do not include any other text.\n\n"
f"Code:\n{state['code']}\n\n"
f"Draft Review:\n{state['draft_review']}"
)
raw = llm.invoke([HumanMessage(content=prompt)]).content
try:
parsed = reflect_parser.parse(raw)
except Exception as e:
# Fallback: simple extraction
parsed = ReflectOutput(pep8=5, type_hints=5, edge_cases=5, naming=5, weakest_criterion="pep8", verdict="needs_revision")
state['criteria_scores'] = {
"pep8": parsed.pep8,
"type_hints": parsed.type_hints,
"edge_cases": parsed.edge_cases,
"naming": parsed.naming,
}
state['weakest_criterion'] = parsed.weakest_criterion
state['verdict'] = parsed.verdict
return state
@tool
def rewrite(state: CodeReviewState) -> CodeReviewState:
"""Rewrite the section of the draft review that addresses the weakest criterion."""
prompt = (
"You are a senior Python reviewer.\n"
"The draft review below has been critiqued. The weakest criterion is {criterion}.\n"
"Rewrite only the part of the review that addresses this criterion, improving it.\n"
"Keep the rest of the review unchanged.\n"
"Return the full updated review.\n\n"
f"Weakest criterion: {state['weakest_criterion']}\n\n"
f"Draft Review:\n{state['draft_review']}"
).format(criterion=state['weakest_criterion'])
updated = llm.invoke([HumanMessage(content=prompt)]).content
state['draft_review'] = updated
state['round'] += 1
return state
# ---------- Graph ----------
builder = StateGraph(CodeReviewState)
builder.add_node("draft_review", draft_review)
builder.add_node("reflect", reflect)
builder.add_node("rewrite", rewrite)
builder.set_entry_point("draft_review")
builder.add_edge("draft_review", "reflect")
builder.add_conditional_edges(
"reflect",
lambda state: "rewrite" if state["verdict"] == "needs_revision" and state["round"] < state["max_rounds"] else "END",
)
builder.add_edge("rewrite", "reflect")
builder.add_edge("END", END)
graph = builder.compile()
# ---------- DeepAgent wrapper ----------
backend = CompositeBackend([
LocalShellBackend(workspace_dir="./workspace"),
FilesystemBackend(),
])
@tool
def run_review(code: str) -> str:
"""Run the LangGraph code review pipeline on the provided code."""
initial_state: CodeReviewState = {
"code": code,
"draft_review": "",
"criteria_scores": {},
"weakest_criterion": "",
"verdict": "",
"round": 0,
"max_rounds": 2,
}
final_state = graph.invoke(initial_state)
return (
f"Initial Draft Review:\n{final_state['draft_review']}\n\n"
f"Scores: {final_state['criteria_scores']}\n"
f"Verdict: {final_state['verdict']}\n"
f"Rounds: {final_state['round']}\n"
)
agent = create_deep_agent(
model=llm,
tools=[run_review],
backend=backend,
system_prompt="You are a code review assistant.",
)
async def main():
code_example = """
def sort_numbers(arr):
return sorted(arr)
"""
result = await agent.ainvoke(
{"messages": [HumanMessage(content=f"Please review this code:\n{code_example}")]},
{"configurable": {"thread_id": "session-1"}},
)
print(result["messages"][-1].content)
if __name__ == "__main__":
asyncio.run(main())