Skip to content

Commit

Permalink
feat: Tiny changes see description
Browse files Browse the repository at this point in the history
  • Loading branch information
WilliamMRS committed Nov 12, 2024
1 parent 7d5676d commit 0eb09ab
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 21 deletions.
2 changes: 1 addition & 1 deletion core/Agents/neo_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
171 changes: 171 additions & 0 deletions core/Agents/tool_agent.py
Original file line number Diff line number Diff line change
@@ -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()
"""
3 changes: 2 additions & 1 deletion core/graphAgent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 9 additions & 17 deletions core/noder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion core/summarize_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,5 @@ def summarize_chat(chat_history):
response_format={
"type": "text"
}
)
)
return response.choices[0].message.content
2 changes: 1 addition & 1 deletion speechToText/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
socketio.run(app, debug=True, host='0.0.0.0', port=3001, allow_unsafe_werkzeug=True)

0 comments on commit 0eb09ab

Please sign in to comment.