Rebuild the task as a single LangGraph code-review reflection graph with structured scores, targeted rewrite, max_rounds guard, and CLI demo.: update main.py

This commit is contained in:
2026-06-18 13:23:17 +00:00
parent 061d1c231e
commit 25cc4c772d
+164 -70
View File
@@ -1,97 +1,168 @@
from typing import TypedDict, Dict import os
import inspect import textwrap
from typing import Literal, TypedDict
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langgraph.graph import END, START, StateGraph
from pydantic import BaseModel, Field
from langgraph.graph import StateGraph, END
from langchain_ollama import Ollama
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import JsonOutputParser
# Define the state
class CodeReviewState(TypedDict): class CodeReviewState(TypedDict):
code: str code: str
draft_review: str draft_review: str
criteria_scores: Dict[str, int] criteria_scores: dict[str, int]
weakest_criterion: str weakest_criterion: str
verdict: str # "ok" | "needs_revision" verdict: str
round: int round: int
max_rounds: int max_rounds: int
# LLM instance (Ollama)
llm = Ollama(model="llama3.1")
# Node: draft_review class ReflectionResult(BaseModel):
criteria_scores: dict[str, int] = Field(
description="Scores from 0 to 10 for pep8, type_hints, edge_cases, naming."
)
weakest_criterion: Literal["pep8", "type_hints", "edge_cases", "naming"]
verdict: Literal["ok", "needs_revision"]
def draft_review(state: CodeReviewState) -> Dict[str, str]:
prompt = ChatPromptTemplate.from_messages([
("system", "You are a senior Python developer. Write a concise code review for the given function. Provide 3-6 actionable points."),
("user", "Here is the function:\n{code}")
])
chain = prompt | llm
review = chain.invoke({"code": state["code"]})
return {"draft_review": review}
# Node: reflect def build_llm() -> ChatOpenAI:
return ChatOpenAI(
model=os.getenv("OPENAI_MODEL", "openai/gpt-oss-20b"),
base_url=os.getenv("OPENAI_BASE_URL"),
api_key=os.getenv("OPENAI_API_KEY", "dummy"),
temperature=0,
)
def reflect(state: CodeReviewState) -> Dict[str, object]:
prompt = ChatPromptTemplate.from_messages([ def draft_review(state: CodeReviewState) -> dict:
("system", """You are a code quality critic. Score the following review on four criteria: PEP8, type hints, edge cases, naming. Return a JSON with integer scores 0-10, the weakest criterion, and verdict \"ok\" or \"needs_revision\".\n""") , llm = build_llm()
("user", "Review:\n{draft_review}") prompt = textwrap.dedent(
]) """
parser = JsonOutputParser() Ты опытный Python code reviewer.
chain = prompt | llm | parser Напиши code review по функции ниже.
result = chain.invoke({"draft_review": state["draft_review"]}) Требования:
# result is a dict - 3-6 конкретных пунктов;
- оцени сильные стороны и что улучшить;
- обязательно затронь PEP8, type hints, edge cases и naming, если это уместно;
- ответ пиши на русском.
Код:
{code}
"""
).strip()
response = llm.invoke(prompt.format(code=state["code"]))
return {"draft_review": response.content.strip()}
def reflect(state: CodeReviewState) -> dict:
llm = build_llm().with_structured_output(ReflectionResult)
prompt = textwrap.dedent(
"""
Ты критик качества code review.
Оцени review по 4 критериям:
- pep8
- type_hints
- edge_cases
- naming
Правила:
- для каждого критерия выставь integer score от 0 до 10;
- weakest_criterion — самый слабый критерий;
- verdict = "ok", если review уже достаточно хорошее;
- verdict = "needs_revision", если самое слабое место стоит усилить.
Код:
{code}
Review:
{review}
"""
).strip()
result = llm.invoke(
prompt.format(code=state["code"], review=state["draft_review"])
)
return { return {
"criteria_scores": { "criteria_scores": result.criteria_scores,
"pep8": result["pep8"], "weakest_criterion": result.weakest_criterion,
"type_hints": result["type_hints"], "verdict": result.verdict,
"edge_cases": result["edge_cases"],
"naming": result["naming"],
},
"weakest_criterion": result["weakest_criterion"],
"verdict": result["verdict"],
} }
# Node: rewrite
def rewrite(state: CodeReviewState) -> Dict[str, str]: def rewrite(state: CodeReviewState) -> dict:
# Increment round llm = build_llm()
state["round"] += 1 prompt = textwrap.dedent(
prompt = ChatPromptTemplate.from_messages([ """
("system", "You are a senior Python developer. Rewrite the review to improve the section about {weakest_criterion}. Keep other points unchanged."), Ты улучшаешь уже написанный code review.
("user", "Original review:\n{draft_review}") Нужно целенаправленно усилить самое слабое место: {weakest_criterion}.
])
chain = prompt | llm
new_review = chain.invoke({"weakest_criterion": state["weakest_criterion"], "draft_review": state["draft_review"]})
return {"draft_review": new_review}
# Build the graph Требования:
- сохрани общий формат краткого review;
- сделай акцент именно на критерии {weakest_criterion};
- добавь более точные и полезные замечания;
- ответ пиши на русском;
- итог должен остаться в формате 3-6 пунктов.
Код:
{code}
Текущий review:
{review}
"""
).strip()
response = llm.invoke(
prompt.format(
weakest_criterion=state["weakest_criterion"],
code=state["code"],
review=state["draft_review"],
)
)
return {
"draft_review": response.content.strip(),
"round": state["round"] + 1,
}
def next_step(state: CodeReviewState) -> str:
if state["verdict"] == "ok":
return "finish"
if state["round"] >= state["max_rounds"]:
return "finish"
return "rewrite"
def build_graph():
builder = StateGraph(CodeReviewState) builder = StateGraph(CodeReviewState)
builder.add_node("draft_review", draft_review) builder.add_node("draft_review", draft_review)
builder.add_node("reflect", reflect) builder.add_node("reflect", reflect)
builder.add_node("rewrite", rewrite) builder.add_node("rewrite", rewrite)
builder.add_edge(START, "draft_review")
builder.add_edge("draft_review", "reflect") builder.add_edge("draft_review", "reflect")
# Conditional edge after reflect
builder.add_conditional_edges( builder.add_conditional_edges(
"reflect", "reflect",
lambda state: "END" if state["verdict"] == "ok" else "rewrite", next_step,
{
"rewrite": "rewrite",
"finish": END,
},
) )
builder.add_edge("rewrite", "reflect") builder.add_edge("rewrite", "reflect")
return builder.compile()
builder.set_entry_point("draft_review")
builder.set_finish_point("END")
graph = builder.compile() def demo_code() -> str:
return textwrap.dedent(
# Demo """
if __name__ == "__main__":
def sort_numbers(arr): def sort_numbers(arr):
return sorted(arr) return sorted(arr)
"""
).strip()
code = inspect.getsource(sort_numbers)
def run_demo() -> None:
graph = build_graph()
initial_state: CodeReviewState = { initial_state: CodeReviewState = {
"code": code, "code": demo_code(),
"draft_review": "", "draft_review": "",
"criteria_scores": {}, "criteria_scores": {},
"weakest_criterion": "", "weakest_criterion": "",
@@ -99,12 +170,35 @@ if __name__ == "__main__":
"round": 0, "round": 0,
"max_rounds": 2, "max_rounds": 2,
} }
result = graph.invoke(initial_state)
print("\n--- Draft Review ---") print("=== Code Review Reflection Demo ===")
print(result["draft_review"]) print("Code under review:")
print("\n--- Scores ---") print(initial_state["code"])
print(result["criteria_scores"])
print("\n--- Verdict ---") final_state = initial_state.copy()
print(result["verdict"]) for chunk in graph.stream(initial_state, stream_mode="updates"):
print("\n--- Round ---") for node_name, update in chunk.items():
print(result["round"]) final_state.update(update)
print()
if node_name == "draft_review":
print("--- Draft Review ---")
print(final_state["draft_review"])
elif node_name == "reflect":
print("--- Critic Scores ---")
print(final_state["criteria_scores"])
print(f"Weakest criterion: {final_state['weakest_criterion']}")
print(f"Verdict: {final_state['verdict']}")
elif node_name == "rewrite":
print(f"--- Rewritten Review After Round {final_state['round']} ---")
print(final_state["draft_review"])
print()
print("=== Final Result ===")
print(final_state["draft_review"])
print(final_state["criteria_scores"])
print(f"Rounds used: {final_state['round']} / {final_state['max_rounds']}")
if __name__ == "__main__":
load_dotenv()
run_demo()