From 00128357495647517ed94b1ac2302848841a1e13 Mon Sep 17 00:00:00 2001 From: William M Schmidt Date: Mon, 11 Nov 2024 23:55:11 +0100 Subject: [PATCH] feat: Added neoagent for testing and as a base-agent Alternative, simple agent for chatting using the ReAct template --- core/Agents/neoagent.py | 118 ++++++++++++++++++++++++++++++++++------ core/graphAgent.py | 18 +----- core/main.py | 4 ++ core/neoagent.png | Bin 0 -> 8804 bytes 4 files changed, 106 insertions(+), 34 deletions(-) create mode 100644 core/neoagent.png diff --git a/core/Agents/neoagent.py b/core/Agents/neoagent.py index 9de9874..7d72e11 100644 --- a/core/Agents/neoagent.py +++ b/core/Agents/neoagent.py @@ -6,7 +6,8 @@ from langchain_core.tools import tool from langgraph.checkpoint.memory import MemorySaver from langgraph.graph import MessagesState, StateGraph, START, END -from langgraph.prebuilt import ToolNode +from langchain_core.messages import BaseMessage, AIMessageChunk, HumanMessage, AIMessage, ToolMessage +from langgraph.prebuilt import ToolNode, tools_condition from models import Model #Models for chatGPT @@ -24,28 +25,111 @@ ReAct is a simple multi-step agent architecture. Smaller graphs are often better understood by the LLMs. """ -# Defining the model TODO: Make this configurable with Llama, Grok, Gemini, Claude -model = ChatOpenAI( - model = Model.gpt_4o, - temperature=0, - max_tokens=16384, # Max tokens for mini. For gpt4o it's 128k -) # Using ChatGPT hardcoded (TODO: Make this dynamic) -# Defining the checkpoint memory saver. -memory = MemorySaver() +class Neoagent: + def __init__(self): + print(""" +------------------------------ +Instantiated NeoAgent.... +------------------------------ + """) + system_prompt = "You are Jarvis, an AI assistant here to help the human accomplish tasks. Respond in a conversational, natural style that sounds good when spoken aloud. Keep responses short and to the point, using clear, engaging language. When explaining your thought process, be concise and only describe essential steps to maintain a conversational flow." + # Defining the model TODO: Make this configurable with Llama, Grok, Gemini, Claude + model = ChatOpenAI( + model = Model.gpt_4o, + temperature=0, + max_tokens=16384, # Max tokens for mini. For gpt4o it's 128k + ) # Using ChatGPT hardcoded (TODO: Make this dynamic) + # Defining the checkpoint memory saver. + memory = MemorySaver() -# Defining the tavily web-search tool -tavily = TavilySearchResults(max_results=2) + # Defining the tavily web-search tool + tavily = TavilySearchResults(max_results=2) -tools = [add, tavily] -tool_node = ToolNode(tools) + # Adding tools and creating the tool node. + tools = [add, tavily] + tool_node = ToolNode(tools) + llm_with_tools = model.bind_tools(tools) -bound_model = model.bind_tools(tools) + class State(TypedDict): + messages: Annotated[list, add_messages] -class State(TypedDict): - messages: Annotated[list, add_messages] + graph_builder = StateGraph(State) -graph_builder = StateGraph(State) + #Executive node that thinks about the problem or query at hand. + def executive_node(state: State): + if not state["messages"]: + state["messages"] = [("system", system_prompt)] + return {"messages": [llm_with_tools.invoke(state["messages"])]} + + graph_builder.add_node("executive_node", executive_node) + graph_builder.add_node("tools", tool_node) # The prebuilt tool node added as "tools" + graph_builder.add_conditional_edges( + "executive_node", + tools_condition, + ) + + # add conditionals, entry point and compile the graph. Exit is defined in the tools node if required. + graph_builder.add_edge("tools", "executive_node") + graph_builder.set_entry_point("executive_node") + self.graph = graph_builder.compile(checkpointer=memory) + + # Draws the graph visually + with open("neoagent.png", 'wb') as f: + f.write(self.graph.get_graph().draw_mermaid_png()) + + # Streams graph updates using websockets. + def stream_graph_updates(self, user_input: str): + config = {"configurable": {"thread_id": "1"}} # TODO: Remove. This is just a placeholder + for event in self.graph.stream({"messages": [("user", user_input)]}, config): + for value in event.values(): + print("Assistant:", value["messages"][-1].content) + + async def run(self, user_prompt: str, socketio): + """ + Run the agent with a user prompt and emit the response and total tokens via socket + """ + + # TODO: Make the chats saved and restored, using this ID as the guiding values. + # Sets the thread_id for the conversation + config = {"configurable": {"thread_id": "1"}} + + try: + input = {"messages": [("human", user_prompt)]} + socketio.emit("start_message", " ") + config = {"configurable": {"thread_id": "1"}} # Thread here is hardcoded for now. + async for event in self.graph.astream_events(input, config, version='v2'): # The config uses the memory checkpoint to save chat state. Only in-memory, not persistent yet. + event_type = event.get('event') + # Focuses only on the 'on_chain_stream'-events. + # There may be better events to base the response on + if event_type == 'on_chain_end' and event['name'] == 'LangGraph': + ai_message = event['data']['output']['messages'][-1] + + if isinstance(ai_message, AIMessage): + print(ai_message) + if 'tool_calls' in ai_message.additional_kwargs: + try: + tool_call = ai_message.additional_kwargs['tool_calls'][0]['function'] + #tool_call_id = ai_message.additional_kwargs['call_tool'][0]['tool_call_id'] + socketio.emit("tool_call", tool_call) + continue + except Exception as e: + return e + + socketio.emit("chunk", ai_message.content) + socketio.emit("tokens", ai_message.usage_metadata['total_tokens']) + continue + + if event_type == 'on_chain_stream' and event['name'] == 'tools': + tool_response = event['data']['chunk']['messages'][-1] + if isinstance(tool_response, ToolMessage): + socketio.emit("tool_response", tool_response.content) + continue + + return "success" + except Exception as e: + print(e) + return e """ # Updating the state requires creating a new state (following state immutability for history and checkpoints) diff --git a/core/graphAgent.py b/core/graphAgent.py index 81be601..0c74ca4 100644 --- a/core/graphAgent.py +++ b/core/graphAgent.py @@ -83,28 +83,12 @@ def run_stream_only(self, user_prompt: str): for chunk in self.llm.stream(user_prompt): yield chunk.content - #for running the agent comment out for testing in terminal async def run(self, user_prompt: str, socketio): """ Run the agent with a user prompt and emit the response and total tokens via socket """ try: - if 'system:' in user_prompt: - user_prompt = user_prompt.replace('system:', 'suggested prompt for my ai:') - - # TODO: Link to the current chatID - # Graph.trim_history to remove excess tokens above the limit. - chat_history = [("human", "How many planets are there in the solar system?"), - ("ai", "There are eight planets in our solar system. They are Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune.")] - - input = {"messages": [ - ("system", """ - You are Jarvis, an AI assistant here to help the human accomplish tasks. - Respond in a conversational, natural style that sounds good when spoken aloud. - Keep responses short and to the point, using clear, engaging language. - When explaining your thought process, be concise and only describe essential steps to maintain a conversational flow. - """) - ] + chat_history + [("human", user_prompt)]} + input = {"messages": [("human", user_prompt)]} socketio.emit("start_message", " ") config = {"configurable": {"thread_id": "1"}} # Thread here is hardcoded for now. async for event in self.graph.astream_events(input, config, version='v2'): # The config uses the memory checkpoint to save chat state. Only in-memory, not persistent yet. diff --git a/core/main.py b/core/main.py index cc81baa..0bb6ae0 100644 --- a/core/main.py +++ b/core/main.py @@ -1,5 +1,6 @@ from flask import Flask, request, url_for, jsonify from graphAgent import Graph +from Agents.neoagent import Neoagent from models import Model from summarize_chat import summarize_chat from rag import embed_and_store @@ -29,7 +30,10 @@ socketio = SocketIO(app, cors_allowed_origins="*") # Enable CORS for WebSocket # Agent instantiation +# Graph() contains all complex tools +# Neoagent() is a simple ReAct agent that only has websearch and the add tool. For testing purposes. jarvis = Graph() # API key is configured in agent.py +#jarvis = Neoagent() # Initialize active_chats with the correct format active_chats = defaultdict(lambda: {"chat_history": []}) diff --git a/core/neoagent.png b/core/neoagent.png new file mode 100644 index 0000000000000000000000000000000000000000..115bf25904bca9e0e7e511b564b0219ac5ee68fd GIT binary patch literal 8804 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 Zp9?>)94QQIg+~Ja{EzFe&DfiX{{nJ4MS}nU literal 0 HcmV?d00001