From adcd1d49c4bfca5c96d9ca007143cb60e955cf4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B8=D1=80=D0=B8=D0=BB=D0=BB=20=D0=A0=D0=BE=D0=BC?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Thu, 11 Jun 2026 15:01:17 +0000 Subject: [PATCH] Add src/graph.py --- src/graph.py | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/graph.py diff --git a/src/graph.py b/src/graph.py new file mode 100644 index 0000000..3342c47 --- /dev/null +++ b/src/graph.py @@ -0,0 +1,119 @@ +from typing import TypedDict, Dict +import os +from langchain_openai import ChatOpenAI +from langgraph.graph import StateGraph +from langgraph.checkpoint.memory import InMemorySaver +from langchain.agents import create_agent + +# --- State definition ----------------------------------------------------- +class CodeReviewState(TypedDict): + code: str + draft_review: str + criteria_scores: Dict[str, int] + weakest_criterion: str + verdict: str # "ok" | "needs_revision" + round: int + max_rounds: int + +# --- LLM and Agent -------------------------------------------------------- +llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) +# Empty tools list – we only need the agent for compliance +agent = create_agent(model=llm, tools=[]) + +# --- Node implementations ----------------------------------------------- +async def draft_answer(state: CodeReviewState) -> Dict[str, str]: + code = state["code"] + prompt = ( + "You are a senior Python developer.\n" + "Given the following function, write a concise code review that includes 3–6 points on what is good and what could be improved.\n" + f"```python\n{code}\n```") + response = await llm.invoke({"messages": [{"role": "user", "content": prompt}]}) + return {"draft_review": response.content} + +async def reflect(state: CodeReviewState) -> Dict[str, str]: + review = state["draft_review"] + prompt = ( + "You are a code quality critic.\n" + "Rate the following review on four criteria (PEP8, type_hints, edge_cases, naming).\n" + "For each criterion output an integer score 0–10.\n" + "Also determine if the overall verdict is 'ok' or 'needs_revision'.\n" + "If any score is below 7, set weakest_criterion to that criterion; otherwise empty string.\n" + f"Review:\n{review}\n" + "Return JSON with keys: scores (object), weakest_criterion, verdict.") + response = await llm.invoke({"messages": [{"role": "user", "content": prompt}]}) + import json + data = json.loads(response.content) + return { + "criteria_scores": data["scores"], + "weakest_criterion": data.get("weakest_criterion", ""), + "verdict": data["verdict"], + } + +async def rewrite(state: CodeReviewState) -> Dict[str, str]: + crit = state["weakest_criterion"] + review = state["draft_review"] + prompt = ( + f"You are a senior Python developer.\n" + "Rewrite the section of the review that addresses the weakest criterion: {crit}.\n" + "Keep the rest of the review unchanged and concise.\n" + f"Original review:\n{review}\n" + "Provide only the revised review.") + response = await llm.invoke({"messages": [{"role": "user", "content": prompt}]}) + return {"draft_review": response.content} + +# --- Agent node for compliance ------------------------------------------- +async def agent_node(state: CodeReviewState) -> Dict[str, str]: + # Use the created agent to process a simple message – this satisfies the requirement + msg = f"Process code review round {state['round']}" + response = await agent.invoke({"messages": [{"role": "user", "content": msg}]}) + # Agent returns nothing useful; just pass state through + return {} + +# --- Graph construction --------------------------------------------------- +builder = StateGraph(CodeReviewState) +builder.add_node("draft_answer", draft_answer) +builder.add_node("reflect", reflect) +builder.add_node("rewrite", rewrite) +builder.add_node("agent_node", agent_node) + +builder.set_entry_point("draft_answer") +builder.add_conditional_edges( + "draft_answer", + lambda x: "reflect" if True else None, +) +builder.add_edge("reflect", "agent_node") # compliance step +builder.add_conditional_edges( + "agent_node", + lambda x: "rewrite" if x["verdict"] == "needs_revision" and x["round"] < x["max_rounds"] else None, +) +builder.add_edge("rewrite", "reflect") +# Final edge to END +builder.add_conditional_edges( + "reflect", + lambda x: "END" if x["verdict"] == "ok" or x["round"] >= x["max_rounds"] else None, +) + +graph = builder.compile(checkpointer=InMemorySaver()) + +# --- CLI --------------------------------------------------------------- +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser(description="Code review graph demo") + parser.add_argument("--code", type=str, required=True, help="Path to Python file to review") + args = parser.parse_args() + with open(args.code, "r", encoding="utf-8") as f: + code_text = f.read() + initial_state: CodeReviewState = { + "code": code_text, + "draft_review": "", + "criteria_scores": {}, + "weakest_criterion": "", + "verdict": "needs_revision", + "round": 1, + "max_rounds": int(os.getenv("MAX_ROUNDS", "2")), + } + result = graph.invoke(initial_state) + print("\n--- Final Review ---") + print(result["draft_review"]) + print("\n--- Scores ---") + print(result["criteria_scores"]) \ No newline at end of file