from langgraph.graph import StateGraph, START, END from langchain_openai import ChatOpenAI from langgraph.checkpoint.memory import MemorySaver from langgraph.prebuilt import ToolNode from chemgraph.tools.graspa_tools import run_graspa from chemgraph.schemas.agent_response import ResponseFormatter from chemgraph.prompt.single_agent_prompt import ( single_agent_prompt, formatter_prompt, ) from chemgraph.utils.logging_config import setup_logger from chemgraph.state.state import State logger = setup_logger(__name__) def route_tools(state: State): """Route to the 'tools' node if the last message has tool calls; otherwise, route to 'done'. Parameters ---------- state : State The current state containing messages and remaining steps Returns ------- str Either 'tools' or 'done' based on the state conditions """ if isinstance(state, list): ai_message = state[-1] elif messages := state.get("messages", []): ai_message = messages[-1] else: raise ValueError(f"No messages found in input state to tool_edge: {state}") if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0: return "tools" return "done" def ChemGraphAgent(state: State, llm: ChatOpenAI, system_prompt: str, tools=None): """LLM node that processes messages and decides next actions. Parameters ---------- state : State The current state containing messages and remaining steps llm : ChatOpenAI The language model to use for processing system_prompt : str The system prompt to guide the LLM's behavior tools : list, optional List of tools available to the agent, by default None Returns ------- dict Updated state containing the LLM's response """ # Load default tools if no tool is specified. if tools is None: tools = [run_graspa] messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": f"{state['messages']}"}, ] llm_with_tools = llm.bind_tools(tools=tools) return {"messages": [llm_with_tools.invoke(messages)]} def ResponseAgent(state: State, llm: ChatOpenAI, formatter_prompt: str): """An LLM agent responsible for formatting final messag Parameters ---------- state : State The current state containing messages and remaining steps llm : ChatOpenAI The language model to use for formatting formatter_prompt : str The prompt to guide the LLM's formatting behavior Returns ------- dict Updated state containing the formatted response """ messages = [ {"role": "system", "content": formatter_prompt}, {"role": "user", "content": f"{state['messages']}"}, ] llm_structured_output = llm.with_structured_output(ResponseFormatter) response = llm_structured_output.invoke(messages).model_dump_json() return {"messages": [response]} def construct_graspa_graph( llm: ChatOpenAI, system_prompt: str = single_agent_prompt, structured_output: bool = False, formatter_prompt: str = formatter_prompt, tools: list = None, ): """Construct a geometry optimization graph. Parameters ---------- llm : ChatOpenAI The language model to use for the graph system_prompt : str, optional The system prompt to guide the LLM's behavior, by default single_agent_prompt structured_output : bool, optional Whether to use structured output, by default False formatter_prompt : str, optional The prompt to guide the LLM's formatting behavior, by default formatter_prompt tool: list, optional The list of tools for the agent, by default None Returns ------- StateGraph The constructed geometry optimization graph """ try: logger.info("Constructing gRASPA graph") checkpointer = MemorySaver() if tools is None: tools = [run_graspa] tool_node = ToolNode(tools=tools) graph_builder = StateGraph(State) if not structured_output: graph_builder.add_node( "ChemGraphAgent", lambda state: ChemGraphAgent( state, llm, system_prompt=system_prompt, tools=tools ), ) graph_builder.add_node("tools", tool_node) graph_builder.add_conditional_edges( "ChemGraphAgent", route_tools, {"tools": "tools", "done": END}, ) graph_builder.add_edge("tools", "ChemGraphAgent") graph_builder.add_edge(START, "ChemGraphAgent") graph = graph_builder.compile(checkpointer=checkpointer) logger.info("gRASPA graph construction completed") return graph else: graph_builder.add_node( "ChemGraphAgent", lambda state: ChemGraphAgent( state, llm, system_prompt=system_prompt, tools=tools ), ) graph_builder.add_node("tools", tool_node) graph_builder.add_node( "ResponseAgent", lambda state: ResponseAgent( state, llm, formatter_prompt=formatter_prompt ), ) graph_builder.add_conditional_edges( "ChemGraphAgent", route_tools, {"tools": "tools", "done": "ResponseAgent"}, ) graph_builder.add_edge("tools", "ChemGraphAgent") graph_builder.add_edge(START, "ChemGraphAgent") graph_builder.add_edge("ResponseAgent", END) graph = graph_builder.compile(checkpointer=checkpointer) logger.info("gRASPA graph construction completed") return graph except Exception as e: logger.error(f"Error constructing graph: {str(e)}") raise