fix(needs_fixes): 1 исправлений, 1 отстояно — main.py
This commit is contained in:
@@ -1,16 +1,15 @@
|
|||||||
import os
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import TypedDict, Annotated, Dict
|
from typing import TypedDict, Annotated, Dict
|
||||||
|
|
||||||
from langchain_openai import ChatOpenAI
|
|
||||||
from langchain_core.messages import HumanMessage
|
|
||||||
from langchain.tools import tool
|
|
||||||
from deepagents import create_deep_agent
|
|
||||||
from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend
|
|
||||||
from langgraph.graph import StateGraph, START, END
|
from langgraph.graph import StateGraph, START, END
|
||||||
from langgraph.graph.message import add_messages
|
from langgraph.graph.message import add_messages
|
||||||
from pydantic import BaseModel, Field
|
from langchain_openai import ChatOpenAI
|
||||||
|
from langchain_core.messages import HumanMessage, AIMessage
|
||||||
from langchain_core.output_parsers import PydanticOutputParser
|
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 ----------
|
||||||
llm = ChatOpenAI(
|
llm = ChatOpenAI(
|
||||||
@@ -20,12 +19,6 @@ llm = ChatOpenAI(
|
|||||||
temperature=0.0,
|
temperature=0.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------- Backend ----------
|
|
||||||
backend = CompositeBackend([
|
|
||||||
LocalShellBackend(workspace_dir="./workspace"),
|
|
||||||
FilesystemBackend(),
|
|
||||||
])
|
|
||||||
|
|
||||||
# ---------- State ----------
|
# ---------- State ----------
|
||||||
class CodeReviewState(TypedDict):
|
class CodeReviewState(TypedDict):
|
||||||
code: str
|
code: str
|
||||||
@@ -36,147 +29,141 @@ class CodeReviewState(TypedDict):
|
|||||||
round: int
|
round: int
|
||||||
max_rounds: int
|
max_rounds: int
|
||||||
|
|
||||||
# ---------- Pydantic models for structured output ----------
|
# ---------- Structured output for reflect ----------
|
||||||
class ReviewScores(BaseModel):
|
class ReflectOutput(BaseModel):
|
||||||
pep8: int = Field(..., ge=0, le=10)
|
pep8: int = Field(..., ge=0, le=10)
|
||||||
type_hints: int = Field(..., ge=0, le=10)
|
type_hints: int = Field(..., ge=0, le=10)
|
||||||
edge_cases: int = Field(..., ge=0, le=10)
|
edge_cases: int = Field(..., ge=0, le=10)
|
||||||
naming: int = Field(..., ge=0, le=10)
|
naming: int = Field(..., ge=0, le=10)
|
||||||
weakest_criterion: str
|
weakest_criterion: str = Field(...)
|
||||||
verdict: str
|
verdict: str = Field(..., regex="^(ok|needs_revision)$")
|
||||||
|
|
||||||
class ReviewRewrite(BaseModel):
|
reflect_parser = PydanticOutputParser(pydantic_object=ReflectOutput)
|
||||||
draft_review: str
|
|
||||||
criteria_scores: Dict[str, int]
|
|
||||||
weakest_criterion: str
|
|
||||||
verdict: str
|
|
||||||
round: int
|
|
||||||
max_rounds: int
|
|
||||||
|
|
||||||
# ---------- Output parsers ----------
|
|
||||||
review_parser = PydanticOutputParser(pydantic_object=ReviewScores)
|
|
||||||
rewrite_parser = PydanticOutputParser(pydantic_object=ReviewRewrite)
|
|
||||||
|
|
||||||
# ---------- Nodes ----------
|
# ---------- Nodes ----------
|
||||||
|
@tool
|
||||||
def draft_review_node(state: CodeReviewState) -> CodeReviewState:
|
def draft_review(state: CodeReviewState) -> CodeReviewState:
|
||||||
code = state["code"]
|
"""Generate an initial code review."""
|
||||||
prompt = f"""
|
prompt = (
|
||||||
You are a senior Python reviewer. Provide a concise code review for the following function. Output exactly 3-6 bullet points, each starting with a dash. Do not include any additional text.
|
"You are a senior Python reviewer.\n"
|
||||||
|
"Given the following function, write a concise code review (3–6 points).\n"
|
||||||
{code}
|
"Focus on style, correctness, and potential improvements.\n"
|
||||||
"""
|
"Return only the review text.\n\n"
|
||||||
response = llm.invoke([HumanMessage(content=prompt)])
|
f"Function:\n{state['code']}"
|
||||||
state["draft_review"] = response.content.strip()
|
)
|
||||||
|
review = llm.invoke([HumanMessage(content=prompt)]).content
|
||||||
|
state['draft_review'] = review
|
||||||
return state
|
return state
|
||||||
|
|
||||||
# DESIGN DECISION: reflect node returns structured JSON with scores and verdict
|
@tool
|
||||||
# NECESSITY: required by assignment to have structured output for automated parsing
|
def reflect(state: CodeReviewState) -> CodeReviewState:
|
||||||
# OPTIMALITY: eliminates ambiguity and parsing errors compared to free text
|
"""Critique the draft review and score four criteria."""
|
||||||
# ALTERNATIVES CONSIDERED: free text parsing, regex extraction – rejected due to unreliability
|
prompt = (
|
||||||
|
"You are an automated code review critic.\n"
|
||||||
def reflect_node(state: CodeReviewState) -> CodeReviewState:
|
"Given the original code and the draft review, assign a score 0–10 for each of the following criteria:\n"
|
||||||
prompt = f"""
|
"- pep8: adherence to PEP8 style guide\n"
|
||||||
You are an automated code quality critic. Evaluate the following draft review against these criteria:
|
"- type_hints: use of type hints\n"
|
||||||
- PEP8 compliance
|
"- edge_cases: handling of edge cases\n"
|
||||||
- Presence of type hints
|
"- naming: clarity of identifiers\n"
|
||||||
- Handling of edge cases
|
"Also identify the weakest criterion and decide if the review is "ok" or "needs_revision".\n"
|
||||||
- Naming conventions
|
"Return a JSON object with keys: pep8, type_hints, edge_cases, naming, weakest_criterion, verdict.\n"
|
||||||
|
"Do not include any other text.\n\n"
|
||||||
Return a JSON object with integer scores 0-10 for each criterion, the name of the weakest criterion, and a verdict "ok" or "needs_revision".
|
f"Code:\n{state['code']}\n\n"
|
||||||
|
f"Draft Review:\n{state['draft_review']}"
|
||||||
Draft review:
|
)
|
||||||
{state["draft_review"]}
|
raw = llm.invoke([HumanMessage(content=prompt)]).content
|
||||||
"""
|
try:
|
||||||
response = llm.invoke([HumanMessage(content=prompt)])
|
parsed = reflect_parser.parse(raw)
|
||||||
parsed = review_parser.parse(response.content)
|
except Exception as e:
|
||||||
state["criteria_scores"] = {
|
# 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,
|
"pep8": parsed.pep8,
|
||||||
"type_hints": parsed.type_hints,
|
"type_hints": parsed.type_hints,
|
||||||
"edge_cases": parsed.edge_cases,
|
"edge_cases": parsed.edge_cases,
|
||||||
"naming": parsed.naming,
|
"naming": parsed.naming,
|
||||||
}
|
}
|
||||||
state["weakest_criterion"] = parsed.weakest_criterion
|
state['weakest_criterion'] = parsed.weakest_criterion
|
||||||
state["verdict"] = parsed.verdict
|
state['verdict'] = parsed.verdict
|
||||||
return state
|
return state
|
||||||
|
|
||||||
# DESIGN DECISION: rewrite node focuses only on weakest criterion
|
@tool
|
||||||
# NECESSITY: assignment specifies targeted rewrite
|
def rewrite(state: CodeReviewState) -> CodeReviewState:
|
||||||
# OPTIMALITY: keeps changes minimal and focused, avoids over‑engineering
|
"""Rewrite the section of the draft review that addresses the weakest criterion."""
|
||||||
# ALTERNATIVES CONSIDERED: full rewrite of review – rejected for unnecessary complexity
|
prompt = (
|
||||||
|
"You are a senior Python reviewer.\n"
|
||||||
def rewrite_node(state: CodeReviewState) -> CodeReviewState:
|
"The draft review below has been critiqued. The weakest criterion is {criterion}.\n"
|
||||||
prompt = f"""
|
"Rewrite only the part of the review that addresses this criterion, improving it.\n"
|
||||||
You are a code reviewer. The previous draft review was:
|
"Keep the rest of the review unchanged.\n"
|
||||||
{state["draft_review"]}
|
"Return the full updated review.\n\n"
|
||||||
|
f"Weakest criterion: {state['weakest_criterion']}\n\n"
|
||||||
The weakest criterion is {state["weakest_criterion"]}. Rewrite only the part of the review that addresses this criterion, improving it. Keep the rest of the review unchanged. Output the updated draft review and updated scores (same format as in reflect). Also increment the round counter.
|
f"Draft Review:\n{state['draft_review']}"
|
||||||
"""
|
).format(criterion=state['weakest_criterion'])
|
||||||
response = llm.invoke([HumanMessage(content=prompt)])
|
updated = llm.invoke([HumanMessage(content=prompt)]).content
|
||||||
parsed = rewrite_parser.parse(response.content)
|
state['draft_review'] = updated
|
||||||
state["draft_review"] = parsed.draft_review
|
state['round'] += 1
|
||||||
state["criteria_scores"] = parsed.criteria_scores
|
|
||||||
state["weakest_criterion"] = parsed.weakest_criterion
|
|
||||||
state["verdict"] = parsed.verdict
|
|
||||||
state["round"] = parsed.round
|
|
||||||
state["max_rounds"] = parsed.max_rounds
|
|
||||||
return state
|
return state
|
||||||
|
|
||||||
# ---------- Graph ----------
|
# ---------- Graph ----------
|
||||||
|
builder = StateGraph(CodeReviewState)
|
||||||
|
builder.add_node("draft_review", draft_review)
|
||||||
|
builder.add_node("reflect", reflect)
|
||||||
|
builder.add_node("rewrite", rewrite)
|
||||||
|
|
||||||
graph = StateGraph(CodeReviewState)
|
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.add_node("draft_review", draft_review_node)
|
graph = builder.compile()
|
||||||
graph.add_node("reflect", reflect_node)
|
|
||||||
graph.add_node("rewrite", rewrite_node)
|
|
||||||
|
|
||||||
# Entry point
|
|
||||||
graph.add_edge(START, "draft_review")
|
|
||||||
graph.add_edge("draft_review", "reflect")
|
|
||||||
# Conditional edges after reflect
|
|
||||||
|
|
||||||
def decide_next(state: CodeReviewState):
|
|
||||||
if state["verdict"] == "ok":
|
|
||||||
return END
|
|
||||||
if state["round"] < state["max_rounds"]:
|
|
||||||
return "rewrite"
|
|
||||||
return END
|
|
||||||
|
|
||||||
graph.add_conditional_edges("reflect", decide_next, {"rewrite": "rewrite", END: END})
|
|
||||||
# After rewrite go back to reflect
|
|
||||||
graph.add_edge("rewrite", "reflect")
|
|
||||||
|
|
||||||
app = graph.compile()
|
|
||||||
|
|
||||||
# ---------- DeepAgent wrapper ----------
|
# ---------- DeepAgent wrapper ----------
|
||||||
agent = create_deep_agent(
|
backend = CompositeBackend([
|
||||||
model=llm,
|
LocalShellBackend(workspace_dir="./workspace"),
|
||||||
tools=[],
|
FilesystemBackend(),
|
||||||
backend=backend,
|
])
|
||||||
system_prompt="You are a code review assistant.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---------- CLI Demo ----------
|
@tool
|
||||||
async def main():
|
def run_review(code: str) -> str:
|
||||||
# Example function to review
|
"""Run the LangGraph code review pipeline on the provided code."""
|
||||||
code = """
|
|
||||||
def sort_numbers(arr):
|
|
||||||
return sorted(arr)
|
|
||||||
"""
|
|
||||||
initial_state: CodeReviewState = {
|
initial_state: CodeReviewState = {
|
||||||
"code": code.strip(),
|
"code": code,
|
||||||
"draft_review": "", # will be filled
|
"draft_review": "",
|
||||||
"criteria_scores": {},
|
"criteria_scores": {},
|
||||||
"weakest_criterion": "",
|
"weakest_criterion": "",
|
||||||
"verdict": "",
|
"verdict": "",
|
||||||
"round": 0,
|
"round": 0,
|
||||||
"max_rounds": 2,
|
"max_rounds": 2,
|
||||||
}
|
}
|
||||||
result = await app.ainvoke(initial_state)
|
final_state = graph.invoke(initial_state)
|
||||||
print("\n--- Final Review ---")
|
return (
|
||||||
print(result["draft_review"])
|
f"Initial Draft Review:\n{final_state['draft_review']}\n\n"
|
||||||
print("\nScores:", result["criteria_scores"])
|
f"Scores: {final_state['criteria_scores']}\n"
|
||||||
print("Verdict:", result["verdict"])
|
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__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
|||||||
Reference in New Issue
Block a user