diff --git a/langgraph.json b/langgraph.json index 2419916..124939a 100644 --- a/langgraph.json +++ b/langgraph.json @@ -1,7 +1,7 @@ { "dockerfile_lines": [], "graphs": { - "reply_gai": "./src/agent/graph.py:graph" + "reply_gai": "./src/chatbot/graph.py:graph" }, "python_version": "3.11", "env": "./.env", diff --git a/pyproject.toml b/pyproject.toml index d5e726f..36b63e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,10 +23,10 @@ requires = ["setuptools>=73.0.0", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools] -packages = ["agent"] +packages = ["chatbot"] [tool.setuptools.package-dir] -"agent" = "src/agent" +"chatbot" = "src/chatbot" [tool.setuptools.package-data] "*" = ["py.typed"] diff --git a/src/agent/__init__.py b/src/chatbot/__init__.py similarity index 100% rename from src/agent/__init__.py rename to src/chatbot/__init__.py diff --git a/src/chatbot/__pycache__/__init__.cpython-311.pyc b/src/chatbot/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..842d22c Binary files /dev/null and b/src/chatbot/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/chatbot/__pycache__/configuration.cpython-311.pyc b/src/chatbot/__pycache__/configuration.cpython-311.pyc new file mode 100644 index 0000000..275c7f0 Binary files /dev/null and b/src/chatbot/__pycache__/configuration.cpython-311.pyc differ diff --git a/src/chatbot/__pycache__/graph.cpython-311.pyc b/src/chatbot/__pycache__/graph.cpython-311.pyc new file mode 100644 index 0000000..91dfca1 Binary files /dev/null and b/src/chatbot/__pycache__/graph.cpython-311.pyc differ diff --git a/src/chatbot/__pycache__/prompts.cpython-311.pyc b/src/chatbot/__pycache__/prompts.cpython-311.pyc new file mode 100644 index 0000000..62c101e Binary files /dev/null and b/src/chatbot/__pycache__/prompts.cpython-311.pyc differ diff --git a/src/chatbot/__pycache__/utils.cpython-311.pyc b/src/chatbot/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000..3099edc Binary files /dev/null and b/src/chatbot/__pycache__/utils.cpython-311.pyc differ diff --git a/src/agent/configuration.py b/src/chatbot/configuration.py similarity index 90% rename from src/agent/configuration.py rename to src/chatbot/configuration.py index 214055d..8ee501f 100644 --- a/src/agent/configuration.py +++ b/src/chatbot/configuration.py @@ -1,9 +1,8 @@ import os -from dataclasses import dataclass, field, fields +from dataclasses import dataclass, fields from typing import Any, Optional from langchain_core.runnables import RunnableConfig -from typing_extensions import Annotated from dataclasses import dataclass @dataclass(kw_only=True) diff --git a/src/agent/graph.py b/src/chatbot/graph.py similarity index 97% rename from src/agent/graph.py rename to src/chatbot/graph.py index 62ad5b3..99dfcc7 100644 --- a/src/agent/graph.py +++ b/src/chatbot/graph.py @@ -9,9 +9,9 @@ from langgraph.store.base import BaseStore from langgraph.graph import END, StateGraph from arcadepy import Arcade -import agent.configuration as configuration -from agent.prompts import CHAT_INSTRUCTIONS -from agent.utils import get_all_tweets +import chatbot.configuration as configuration +from chatbot.prompts import CHAT_INSTRUCTIONS +from chatbot.utils import get_all_tweets def get_tweets(state: MessagesState, config: RunnableConfig, store: BaseStore) -> dict: """Fetch and store recent tweets for a specified Twitter user. diff --git a/src/agent/prompts.py b/src/chatbot/prompts.py similarity index 86% rename from src/agent/prompts.py rename to src/chatbot/prompts.py index 2cf2cf3..4603963 100644 --- a/src/agent/prompts.py +++ b/src/chatbot/prompts.py @@ -1,4 +1,6 @@ -CHAT_INSTRUCTIONS = """Hey! You're chatting as @{username} on Twitter. I've pulled some of their recent tweets to help you get their vibe: +CHAT_INSTRUCTIONS = """Hey! You're chatting as @{username} on Twitter. + +I've pulled some of their recent tweets to help you get their vibe: {tweets} diff --git a/src/chatbot/reply_gai.ipynb b/src/chatbot/reply_gai.ipynb new file mode 100644 index 0000000..16d920a --- /dev/null +++ b/src/chatbot/reply_gai.ipynb @@ -0,0 +1,259 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "%%capture --no-stderr\n", + "%pip install --quiet -U langgraph langchain_community langchain_core arcade_x" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [], + "source": [ + "import uuid\n", + "import os\n", + "from datetime import datetime, timezone\n", + "from langchain_core.runnables import RunnableConfig\n", + "from langchain_core.messages import SystemMessage\n", + "from langchain_anthropic import ChatAnthropic \n", + "from langgraph.graph import MessagesState\n", + "from langgraph.store.base import BaseStore\n", + "from langgraph.graph import END, StateGraph\n", + "from arcadepy import Arcade\n", + "\n", + "import agent.configuration as configuration\n", + "from agent.prompts import CHAT_INSTRUCTIONS\n", + "from agent.utils import get_all_tweets\n", + "\n", + "def get_tweets(state: MessagesState, config: RunnableConfig, store: BaseStore) -> dict:\n", + " \"\"\"Fetch and store recent tweets for a specified Twitter user.\n", + " \n", + " This function authenticates with the Arcade API, retrieves recent tweets for a given\n", + " username, and stores them in the provided BaseStore instance. Each tweet is stored\n", + " with its text content and URL.\n", + " \n", + " Args:\n", + " state (MessagesState): Current conversation state (unused but required by graph)\n", + " config (RunnableConfig): Configuration object containing settings like username\n", + " store (BaseStore): Storage interface for saving retrieved tweets\n", + " \n", + " Returns:\n", + " dict: Empty dictionary (function stores tweets but doesn't return them)\n", + " \n", + " Note:\n", + " - Requires ARCADE_USER_ID environment variable to be set\n", + " - Fetches up to 100 most recent tweets from the last 7 days\n", + " - Stores tweets using (username, \"tweets\") as namespace\n", + " \"\"\"\n", + "\n", + " # Get the configuration\n", + " configurable = configuration.Configuration.from_runnable_config(config)\n", + "\n", + " client = Arcade() \n", + " USER_ID = os.environ[\"ARCADE_USER_ID\"]\n", + " TOOL_NAME = \"X.SearchRecentTweetsByUsername\"\n", + "\n", + " auth_response = client.tools.authorize(\n", + " tool_name=TOOL_NAME,\n", + " user_id=USER_ID,\n", + " )\n", + "\n", + " if auth_response.status != \"completed\":\n", + " print(f\"Click this link to authorize: {auth_response.authorization_url}\")\n", + "\n", + " # Wait for the authorization to complete\n", + " client.auth.wait_for_completion(auth_response)\n", + "\n", + " # Search for recent tweets (last 7 days) on X (Twitter)\n", + " username = configurable.username\n", + "\n", + " # Get all the tweets\n", + " tweets = get_all_tweets(client, username, USER_ID, TOOL_NAME)\n", + "\n", + " # Load the tweets into memory\n", + " namespace_for_memory = (username, \"tweets\")\n", + " for tweet in tweets:\n", + " memory_id = tweet.get('id',uuid.uuid4())\n", + " text = tweet.get('text',\"Tweet empty\")\n", + " url = tweet.get('tweet_url',\"URL not found\")\n", + " store.put(namespace_for_memory, memory_id, {\"text\": text,\"url\": url})\n", + "\n", + "def chat(state: MessagesState, config: RunnableConfig, store: BaseStore) -> dict:\n", + " \"\"\"Generate a chat response in the style of a specific Twitter user.\n", + " \n", + " This function retrieves tweets from the store for a given username, formats them,\n", + " and uses them as context for Claude to generate a response that mimics the user's\n", + " writing style and personality.\n", + "\n", + " Args:\n", + " state (MessagesState): Current conversation state containing message history\n", + " config (RunnableConfig): Configuration object containing settings like username\n", + " store (BaseStore): Storage interface for accessing saved tweets\n", + "\n", + " Returns:\n", + " dict: Contains the generated message in the 'messages' key\n", + " \"\"\"\n", + "\n", + " # Get the configuration\n", + " configurable = configuration.Configuration.from_runnable_config(config)\n", + " username = configurable.username\n", + " \n", + " # Get the tweets\n", + " namespace_for_memory = (username, \"tweets\")\n", + "\n", + " # Get all the tweets\n", + " memories = []\n", + " while mems := store.search(namespace_for_memory, limit=200, offset=len(memories)):\n", + " memories.extend(mems)\n", + " \n", + " # Format the tweets\n", + " formatted_output = \"\"\n", + " for memory in memories:\n", + " tweet = memory.value\n", + " formatted_output += f\"@{username}: {tweet['text']}\\n\"\n", + " formatted_output += \"-\" * 80 + \"\\n\"\n", + "\n", + " # Generate a response\n", + " claude_3_5_sonnet = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\", temperature=0.75) \n", + " chat_instructions_formatted = CHAT_INSTRUCTIONS.format(username=username,tweets=formatted_output)\n", + " msg = claude_3_5_sonnet.invoke([SystemMessage(content=chat_instructions_formatted)]+state['messages'])\n", + " return {\"messages\": [msg]} \n", + "\n", + "def route_to_tweet_loader(state: MessagesState, config: RunnableConfig, store: BaseStore) -> dict:\n", + " \"\"\"Route the workflow based on tweet availability and age.\n", + " \n", + " This function determines whether to fetch new tweets or proceed to chat by checking:\n", + " 1. If tweets exist for the user in the store\n", + " 2. If existing tweets are too old (beyond max_tweet_age_seconds)\n", + " \n", + " Args:\n", + " state (MessagesState): Current conversation state\n", + " config (RunnableConfig): Configuration containing username and tweet age settings\n", + " store (BaseStore): Storage interface for accessing saved tweets\n", + " \n", + " Returns:\n", + " str: Either \"get_tweets\" to fetch new tweets or \"chat\" to proceed with conversation\n", + " \"\"\"\n", + "\n", + " # Get the configuration\n", + " configurable = configuration.Configuration.from_runnable_config(config)\n", + " username = configurable.username\n", + " \n", + " # If we have Tweets from the user, go to chat\n", + " namespace_for_memory = (username, \"tweets\")\n", + " memories = store.search(namespace_for_memory, limit=200)\n", + "\n", + " # If we have tweets, check if they're too old\n", + " if memories: \n", + " # Get most recent tweet timestamp\n", + " most_recent = max(mem.created_at for mem in memories)\n", + " \n", + " # Calculate time difference\n", + " now = datetime.now(timezone.utc)\n", + " time_delta = now - most_recent\n", + " \n", + " # If tweets are too old, get new ones\n", + " if time_delta.total_seconds() > configurable.max_tweet_age_seconds:\n", + " return \"get_tweets\"\n", + " return \"chat\"\n", + " # If no tweets for the user, get them \n", + " else:\n", + " return \"get_tweets\"\n", + "\n", + "# Create the graph + all nodes\n", + "builder = StateGraph(MessagesState, config_schema=configuration.Configuration)\n", + "builder.add_node(\"chat\",chat)\n", + "builder.add_node(\"get_tweets\",get_tweets)\n", + "builder.set_conditional_entry_point(route_to_tweet_loader, [\"chat\", \"get_tweets\"])\n", + "builder.add_edge(\"get_tweets\", \"chat\")\n", + "builder.add_edge(\"chat\", END)\n", + "\n", + "# Store\n", + "from langgraph.store.memory import InMemoryStore\n", + "in_memory_store = InMemoryStore()\n", + "\n", + "# Compile the graph\n", + "graph = builder.compile(store=in_memory_store)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'messages': [HumanMessage(content='What are some of your favorite applications of LLMs?', additional_kwargs={}, response_metadata={}, id='ca0873c2-fe1e-4d67-a106-46458c8c0820'),\n", + " AIMessage(content=\"Oh man, I love this question! LLMs are just mind-blowing in terms of their potential applications. One of my absolute favorites, which I've been geeking out about recently, is using LLMs as reading companions for books. \\n\\nImagine you're diving into a complex non-fiction book or a dense sci-fi novel, and you've got this AI sidekick that's read the whole thing, understands the context, and can chat with you about it in real-time. You could ask questions, get summaries, or even have generated discussions about themes and ideas. It's like NotebookLM on steroids!\\n\\nI actually tweeted about this recently - I think if Amazon built something like this into Kindle, it would be absolutely game-changing. They've got all the content right there, after all.\\n\\nBeyond that, I'm really excited about LLMs in coding assistants. The potential for accelerating software development is huge. And of course, there's the whole world of creative applications - writing, brainstorming, even aiding in scientific research and hypothesis generation.\\n\\nBut honestly, we're just scratching the surface. The most mind-blowing applications of LLMs are probably things we haven't even thought of yet. It's like we've discovered fire and we're still figuring out all the ways we can use it. Exciting times!\", additional_kwargs={}, response_metadata={'id': 'msg_01Xpv4yY4jLc3EEBpgk52SfR', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 1128, 'output_tokens': 299}}, id='run-c6abe3f1-7cf4-4f55-a6fc-270a6186693a-0', usage_metadata={'input_tokens': 1128, 'output_tokens': 299, 'total_tokens': 1427, 'input_token_details': {}})]}" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "config = {\"configurable\": {\"username\": \"karpathy\"}}\n", + "graph.invoke({\"messages\": [HumanMessage(content=\"What are some of your favorite applications of LLMs?\")]}, config=config)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "14\n" + ] + } + ], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "reply_gai_env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/agent/utils.py b/src/chatbot/utils.py similarity index 100% rename from src/agent/utils.py rename to src/chatbot/utils.py