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:
@@ -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
|
Требования:
|
||||||
builder = StateGraph(CodeReviewState)
|
- сохрани общий формат краткого review;
|
||||||
builder.add_node("draft_review", draft_review)
|
- сделай акцент именно на критерии {weakest_criterion};
|
||||||
builder.add_node("reflect", reflect)
|
- добавь более точные и полезные замечания;
|
||||||
builder.add_node("rewrite", rewrite)
|
- ответ пиши на русском;
|
||||||
|
- итог должен остаться в формате 3-6 пунктов.
|
||||||
|
|
||||||
builder.add_edge("draft_review", "reflect")
|
Код:
|
||||||
# Conditional edge after reflect
|
{code}
|
||||||
builder.add_conditional_edges(
|
|
||||||
|
Текущий 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.add_node("draft_review", draft_review)
|
||||||
|
builder.add_node("reflect", reflect)
|
||||||
|
builder.add_node("rewrite", rewrite)
|
||||||
|
builder.add_edge(START, "draft_review")
|
||||||
|
builder.add_edge("draft_review", "reflect")
|
||||||
|
builder.add_conditional_edges(
|
||||||
"reflect",
|
"reflect",
|
||||||
lambda state: "END" if state["verdict"] == "ok" else "rewrite",
|
next_step,
|
||||||
)
|
{
|
||||||
builder.add_edge("rewrite", "reflect")
|
"rewrite": "rewrite",
|
||||||
|
"finish": END,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user