diff --git a/core/Agents/neo_agent.py b/core/Agents/neo_agent.py index 6bf36d5..3dff54a 100644 --- a/core/Agents/neo_agent.py +++ b/core/Agents/neo_agent.py @@ -60,7 +60,7 @@ class State(TypedDict): graph_builder = StateGraph(State) - #Executive node that thinks about the problem or query at hand. + #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)] diff --git a/core/Agents/tool_agent.py b/core/Agents/tool_agent.py new file mode 100644 index 0000000..20ec321 --- /dev/null +++ b/core/Agents/tool_agent.py @@ -0,0 +1,171 @@ +from typing import Annotated +from typing_extensions import TypedDict +import os + +from langgraph.graph.message import add_messages +from langchain_openai import ChatOpenAI +from langchain_core.tools import tool +from langgraph.checkpoint.memory import MemorySaver +from langgraph.graph import MessagesState, StateGraph, START, END +from langchain_core.messages import BaseMessage, AIMessageChunk, HumanMessage, AIMessage, ToolMessage +from langgraph.prebuilt import ToolNode, tools_condition + +from models import Model #Models for chatGPT + +# Premade tool imports +from langchain_community.tools.tavily_search import TavilySearchResults +# Custom tool imports +from tools.add_tool import add # Adds 2 numbers together + +""" +Neoagent uses the ReAct agent framework. +Simply put in steps: +1. 'Re' The agent reasons about the problem, and plans out steps to solve it. +2. 'Act' The agent acts upon the information gathered. Calling tools or interacting with systems based on the earlier reasoning. +3. 'Loop' If the problem is not adequately solved, the agent can reason and act recursively until a satisfying solution is reached. +ReAct is a simple multi-step agent architecture. +Smaller graphs are often better understood by the LLMs. +""" +class ToolAgent: + 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() + # Tools list + tools = [add] + + if os.getenv("TAVILY_API_KEY"): + # Defining the tavily web-search tool + tavily = TavilySearchResults(max_results=2) + tools = [add, tavily] + else: + print("TAVILY_API_KEY does not exist.") + + tool_node = ToolNode(tools) + llm_with_tools = model.bind_tools(tools) + + class State(TypedDict): + messages: Annotated[list, add_messages] + + 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) + +# Example function to increment a value +def increment_count(state: GraphState) -> GraphState: + return GraphState(count=state["count"] + 1) + +# To add a message to the state. +def add_message(state: GraphState, message: str, is_human: bool = True) -> GraphState: + new_message = HumanMessage(content=message) if is_human else AIMessage(content=message) + return GraphState( + count=state["count"], + messages=state["messages"] + [new_message] + ) + +from langgraph.graph import StateGraph, END + +def create_complex_graph(): + workflow = StateGraph(GraphState) + + def process_message(state: GraphState): + last_message = state["messages"][-1].content if state["messages"] else "No messages yet" + response = f"Received: {last_message}. Count is now {state['count'] + 1}" + return { + "count": state["count"] + 1, + "messages": state["messages"] + [AIMessage(content=response)] + } + + workflow.add_node("process", process_message) + workflow.set_entry_point("process") + workflow.add_edge("process", END) + + return workflow.compile() +""" \ No newline at end of file diff --git a/core/graphAgent.py b/core/graphAgent.py index 1accf4c..7b10bce 100644 --- a/core/graphAgent.py +++ b/core/graphAgent.py @@ -114,7 +114,8 @@ async def run(self, user_prompt: str, socketio): except Exception as e: return e - socketio.emit("chunk", ai_message.content) + socketio.emit("chunk", ai_message.content) # Emits the entire message over the websocket event "chunk" + # TODO: POST REQUEST TO TTS socketio.emit("tokens", ai_message.usage_metadata['total_tokens']) continue diff --git a/core/noder.py b/core/noder.py index bbaf790..ad9d2f9 100644 --- a/core/noder.py +++ b/core/noder.py @@ -10,28 +10,20 @@ def jarvis_agent(state: GraphState): """Agent to determine how to answer user question""" prompt = PromptTemplate( template= """ - Your job is to determine if you need tools to answer the - users question and answer with only the name of the option - chosen. You have access to chat history using tools, thus also some personal data can be retrieved. - som times you have to use multiple tools from multiple diffrent tools that has been called to complte the users requests. - if you calender is sent to you for a second or third time you should generate instead of using tools. - if you get a complex task you should call a tool to help you solve the task. - Here are previous messages: + Determine if the task at hand requires more information you don't already know. + Send the task at hand to + + You must respond with either 'use_tool' or 'generate'. + - 'use_tool': Call on tools to help solve the users problem + - 'generate': Generate a response if you have what you need to answer + Never ever under any condition respond with an empty string. + Message: {messages} Data currently accumulated: Data: {data} - - Your options are the following: - - 'use_tool': Call on tools to help solve the users problem - - 'generate': Generate a response if you have what you need to answer - - Answer with the option name and nothing else there should not be any ' or " in the answer. - You can never answer with an empty string. - please never answer with an empty string. - if you answer with an ampty string you will not do anything and stop working. """, ) chain = prompt | ToolsAgent.agent | StrOutputParser() @@ -146,7 +138,7 @@ def calendar_decision_agent(state: GraphState): jarvis agents question and answer with only the name of the option choose. if you cant find a calendar event you should create a calendar event. - you should create a claender event or read calendar events. if the user has asked for it. + you should create a calendar event or read calendar events. if the user has asked for it. if you have searched for calendar events atleast once you should probably return to jarvis. the same is for creatting a event, you only need to create that event once. and return to jarvis. diff --git a/core/summarize_chat.py b/core/summarize_chat.py index eb715f9..f82f1f5 100644 --- a/core/summarize_chat.py +++ b/core/summarize_chat.py @@ -73,5 +73,5 @@ def summarize_chat(chat_history): response_format={ "type": "text" } - ) + ) return response.choices[0].message.content \ No newline at end of file diff --git a/speechToText/main.py b/speechToText/main.py index e03b16e..33a62a2 100644 --- a/speechToText/main.py +++ b/speechToText/main.py @@ -91,4 +91,4 @@ def upload_audio(): if __name__ == '__main__': if not os.path.exists('uploads'): os.makedirs('uploads') - socketio.run(app, debug=True, host='0.0.0.0', port=PORT_STT, allow_unsafe_werkzeug=True) \ No newline at end of file + socketio.run(app, debug=True, host='0.0.0.0', port=3001, allow_unsafe_werkzeug=True) \ No newline at end of file