diff --git a/README.md b/README.md index 4b17984..746cf6e 100644 --- a/README.md +++ b/README.md @@ -93,3 +93,4 @@ Disclaimer: Examples contributed by the community and partners do not represent | [`x mistral`: CLI & TUI APP Module in X-CMD](third_party/x-cmd/README.md) | CLI, TUI APP, Chat | x-cmd | | [Incremental Prompt Engineering and Model Comparison](third_party/Pixeltable/README.md) | Prompt Engineering, Evaluation | Pixeltable | | [Build a bank support agent with Pydantic AI and Mistral AI](third_party/PydanticAI/pydantic_bank_support_agent.ipynb)| Agent | Pydantic | +| [Analyzing Reddit Comments Sentiment using MistralAI and LangGraph](third_party/langchain/reddit_comments_sentiment_agent_mistral.ipynb)| Agent, Structured Output, Graph | Langchain | \ No newline at end of file diff --git a/third_party/langchain/reddit_comments_sentiment_agent_mistral.ipynb b/third_party/langchain/reddit_comments_sentiment_agent_mistral.ipynb new file mode 100644 index 0000000..9b3d488 --- /dev/null +++ b/third_party/langchain/reddit_comments_sentiment_agent_mistral.ipynb @@ -0,0 +1,851 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Analyzing Reddit Comments Sentiment using MistralAI and LangGraph" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Overview\n", + "\n", + "This notebook was created by Shay Elmualem ([Github](https://github.com/norbinsh/), [Linkedin](https://www.linkedin.com/in/shay-elmualem/))\n", + "This notebook analyzes the sentiment of Reddit post comments using LangGraph and MistralAI's large language model (LLM).\n", + "It fetches Reddit posts, processes comments, performs sentiment analysis, and visualizes the results through a graph-based workflow." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Brief\n", + "\n", + "- Fetches a Reddit post and its comments.\n", + "- Analyzes each comment's sentiment using MistralAI's LLM.\n", + "- Aggregates sentiments to determine the overall sentiment of the discussion.\n", + "- Visualizes the workflow and results with LangGraph and Rich." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Components\n", + "\n", + "- Reddit Client: Uses praw to interact with Reddit's API.\n", + "- Data Models: Structured representations with pydantic.\n", + "- Language Model: MistralAI's LLM for sentiment analysis.\n", + "- Prompt Template: Guides the LLM for consistent analysis.\n", + "- Graph Workflow: Managed by LangGraph for process sequencing.\n", + "- Visualization: Displays results using rich." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup and Installation\n", + "\n", + "Install necessary packages with:" + ] + }, + { + "cell_type": "code", + "execution_count": 121, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n", + "Note: you may need to restart the kernel to use updated packages.\n", + "Note: you may need to restart the kernel to use updated packages.\n", + "Note: you may need to restart the kernel to use updated packages.\n", + "Note: you may need to restart the kernel to use updated packages.\n", + "Note: you may need to restart the kernel to use updated packages.\n", + "Note: you may need to restart the kernel to use updated packages.\n", + "Requirement already satisfied: rich==13.9.4 in ./.venv/lib/python3.9/site-packages (13.9.4)\n", + "Requirement already satisfied: markdown-it-py>=2.2.0 in ./.venv/lib/python3.9/site-packages (from rich==13.9.4) (3.0.0)\n", + "Requirement already satisfied: pygments<3.0.0,>=2.13.0 in ./.venv/lib/python3.9/site-packages (from rich==13.9.4) (2.18.0)\n", + "Requirement already satisfied: typing-extensions<5.0,>=4.0.0 in ./.venv/lib/python3.9/site-packages (from rich==13.9.4) (4.12.2)\n", + "Requirement already satisfied: mdurl~=0.1 in ./.venv/lib/python3.9/site-packages (from markdown-it-py>=2.2.0->rich==13.9.4) (0.1.2)\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "%pip install -q langchain==0.3.13\n", + "%pip install -q langchain_community==0.3.13\n", + "%pip install -q langchain-mistralai==0.2.4\n", + "%pip install -q langgraph==0.2.60\n", + "%pip install -q praw==7.8.1\n", + "%pip install -q python-dotenv==1.0.1\n", + "%pip install -q pydantic==2.10.4\n", + "%pip install rich==13.9.4" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Required Environment Variables\n", + "\n", + "Create a .env file with:\n", + "\n", + "- MISTRAL_API_KEY\n", + "- REDDIT_PRAW_CLIENT_ID\n", + "- REDDIT_PRAW_CLIENT_SECRET\n", + "\n", + "Or use the helper function in the notebook to set them interactively." + ] + }, + { + "cell_type": "code", + "execution_count": 122, + "metadata": {}, + "outputs": [], + "source": [ + "from dotenv import load_dotenv\n", + "import os, getpass\n", + "load_dotenv()\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "_set_env(\"MISTRAL_API_KEY\")\n", + "_set_env(\"REDDIT_PRAW_CLIENT_ID\")\n", + "_set_env(\"REDDIT_PRAW_CLIENT_SECRET\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Imports\n", + "\n", + "All necessary libraries are imported at the beginning for clarity:" + ] + }, + { + "cell_type": "code", + "execution_count": 123, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import re\n", + "import json\n", + "\n", + "import praw\n", + "\n", + "from rich import print\n", + "from rich.console import Console\n", + "from rich.table import Table\n", + "import textwrap\n", + "\n", + "from typing import TypedDict, Annotated, List, Optional, Dict, Literal\n", + "from pydantic import BaseModel, HttpUrl, ValidationError, Field, RootModel\n", + "\n", + "from langgraph.graph import Graph, END\n", + "from langchain_core.prompts import PromptTemplate\n", + "from langchain_core.output_parsers import JsonOutputParser\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "from langchain_core.runnables.graph import MermaidDrawMethod\n", + "from langchain_core.tools import StructuredTool\n", + "\n", + "from IPython.display import display, Image as IPImage\n", + "\n", + "from langchain_mistralai import ChatMistralAI\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Reddit Client Setup\n", + "\n", + "Initialize the Reddit client with praw:" + ] + }, + { + "cell_type": "code", + "execution_count": 124, + "metadata": {}, + "outputs": [], + "source": [ + "REDDIT_USER_AGENT = \"RedditCommentsSentimentAgent\"\n", + "MAX_COMMENT_SAMPLES = 10\n", + "MAX_EXPANDED_COMMENTS = 0\n", + "\n", + "reddit = praw.Reddit(\n", + " client_id=os.environ.get(\"REDDIT_PRAW_CLIENT_ID\"),\n", + " client_secret=os.environ.get(\"REDDIT_PRAW_CLIENT_SECRET\"),\n", + " user_agent=REDDIT_USER_AGENT,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Data Models\n", + "\n", + "Define Reddit's structured data models using pydantic:" + ] + }, + { + "cell_type": "code", + "execution_count": 125, + "metadata": {}, + "outputs": [], + "source": [ + "class Comment(BaseModel):\n", + " body: str\n", + "\n", + "class CommentSentiment(BaseModel):\n", + " sentiment_reason: str\n", + " sentiment_score: float # Ranging from -1 to 1, where -1 is very negative, 0 is neutral, and 1 is very positive\n", + "\n", + "class CommentSentiments(BaseModel):\n", + " sentiments: List[CommentSentiment]\n", + "\n", + "class RedditPost(BaseModel):\n", + " title: str\n", + " body: str\n", + " comments: List[Comment]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Reddit Data Handling Functions\n", + "\n", + "Functions to extract and fetch Reddit posts:" + ] + }, + { + "cell_type": "code", + "execution_count": 126, + "metadata": {}, + "outputs": [], + "source": [ + "def extract_post_id(url: HttpUrl) -> str:\n", + " pattern = r\"comments/([\\w\\d]+)/\"\n", + " match = re.search(pattern, str(url))\n", + " if match:\n", + " return match.group(1)\n", + " else:\n", + " raise ValueError(\"Invalid Reddit post URL.\")\n", + "\n", + "def fetch_post(post_id: str) -> RedditPost:\n", + " post = reddit.submission(id=post_id)\n", + " post.comments.replace_more(limit=MAX_EXPANDED_COMMENTS)\n", + " comments = [\n", + " Comment.model_validate({\"body\": comment.body})\n", + " for comment in post.comments.list()[:MAX_COMMENT_SAMPLES]\n", + " ]\n", + "\n", + " return RedditPost(\n", + " title=post.title,\n", + " body=post.selftext,\n", + " comments=comments\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Language Model Configuration\n", + "\n", + "Configure MistralAI's LLM:" + ] + }, + { + "cell_type": "code", + "execution_count": 127, + "metadata": {}, + "outputs": [], + "source": [ + "mistral_model = \"mistral-large-latest\"\n", + "llm = ChatMistralAI(model=mistral_model, temperature=0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Testing the LLM\n", + "\n", + "Verify LLM functionality:" + ] + }, + { + "cell_type": "code", + "execution_count": 128, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Life on Mars is a fascinating topic. Here are some brief thoughts:\n",
+       "\n",
+       "- **Past Life**: Many scientists believe that Mars had liquid water and a denser atmosphere billions of years ago, \n",
+       "which could have supported microbial life.\n",
+       "- **Present Life**: Current conditions are harsh, but some extremophiles on Earth could theoretically survive \n",
+       "Martian conditions, so it's not ruled out.\n",
+       "- **Future Life**: Mars could potentially be habitable for humans with terraforming, but that's a long-term and \n",
+       "controversial prospect.\n",
+       "- **Missions**: Rovers like Perseverance are currently exploring Mars to seek signs of ancient life and prepare for\n",
+       "future human exploration.\n",
+       "
\n" + ], + "text/plain": [ + "Life on Mars is a fascinating topic. Here are some brief thoughts:\n", + "\n", + "- **Past Life**: Many scientists believe that Mars had liquid water and a denser atmosphere billions of years ago, \n", + "which could have supported microbial life.\n", + "- **Present Life**: Current conditions are harsh, but some extremophiles on Earth could theoretically survive \n", + "Martian conditions, so it's not ruled out.\n", + "- **Future Life**: Mars could potentially be habitable for humans with terraforming, but that's a long-term and \n", + "controversial prospect.\n", + "- **Missions**: Rovers like Perseverance are currently exploring Mars to seek signs of ancient life and prepare for\n", + "future human exploration.\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "test_llm = llm.invoke(\"thoughts on life on mars? keep it very brief :)\").content\n", + "print(test_llm)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Sentiment Analysis Pipeline" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prompt Template\n", + "\n", + "Define the prompt for sentiment analysis:" + ] + }, + { + "cell_type": "code", + "execution_count": 129, + "metadata": {}, + "outputs": [], + "source": [ + "template = \"\"\"\n", + "You are an expert in sentiment analysis, specifically for social media content. Your task is to analyze the sentiment of comments on a Reddit post, considering the context of the original post.\n", + "\n", + "Here are the comments you'll be analyzing:\n", + "\n", + "{comments}\n", + "\n", + "Now, read the Reddit post these comments are responding to:\n", + "\n", + "Title: {reddit_post_title}\n", + "\n", + "Body: {reddit_post_body}\n", + "\n", + "For each comment, follow these steps:\n", + "\n", + "1. Read the comment carefully.\n", + "2. Consider how the comment relates to the original post.\n", + "3. Analyze the sentiment, taking into account:\n", + " - The language used\n", + " - Any emotional expressions\n", + " - The context of the original post\n", + "4. Identify and quote key phrases that indicate sentiment.\n", + "5. Consider both positive and negative aspects of the comment.\n", + "6. Determine a sentiment score from -1 (very negative) to 1 (very positive), where 0 is neutral.\n", + "7. Provide a brief explanation of the sentiment.\n", + "\n", + "IMPORTANT: Respond ONLY with a JSON object containing a \"sentiments\" key. The value of \"sentiments\" should be an array of objects, each representing the analysis for a comment. The response should NOT include any other fields like 'post_summary'.\n", + "\n", + "The JSON object should be in the following format:\n", + "{{\n", + " \"sentiments\": [\n", + " {{\n", + " \"sentiment_reason\": \"Reason for sentiment\",\n", + " \"sentiment_score\": 0.5\n", + " }},\n", + " {{\n", + " \"sentiment_reason\": \"Another reason\",\n", + " \"sentiment_score\": -0.3\n", + " }}\n", + " ]\n", + "}}\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Structured Output\n", + "\n", + "Ensure consistent JSON output:" + ] + }, + { + "cell_type": "code", + "execution_count": 130, + "metadata": {}, + "outputs": [], + "source": [ + "structured_llm = llm.with_structured_output(CommentSentiments, method=\"json_mode\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Graph State Management\n", + "\n", + "Define the graph's state structure:" + ] + }, + { + "cell_type": "code", + "execution_count": 131, + "metadata": {}, + "outputs": [], + "source": [ + "class GraphState(TypedDict):\n", + " url: str\n", + " post: RedditPost\n", + " comment_sentiments: CommentSentiments\n", + " overall_sentiment: float\n", + " is_valid: bool" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Agent Functions\n", + "\n", + "Functions executed by the agent:" + ] + }, + { + "cell_type": "code", + "execution_count": 132, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "\n", + "def fetch_reddit_content(state: GraphState) -> GraphState:\n", + " post_id = extract_post_id(state[\"url\"])\n", + " fetched_post = fetch_post(post_id)\n", + " assert isinstance(fetched_post, RedditPost), f\"fetch_post returned {type(fetched_post)}\"\n", + " state[\"post\"] = fetched_post\n", + " return state\n", + "\n", + "def validate_reddit_content(state: GraphState) -> GraphState:\n", + " state[\"is_valid\"] = bool(state[\"post\"].comments)\n", + " return state\n", + "\n", + "def determine_path(state: GraphState) -> str:\n", + " return \"analyze\" if state[\"is_valid\"] else END\n", + "\n", + "def calculate_overall_sentiment(state: GraphState) -> GraphState:\n", + " sentiments = [s.sentiment_score for s in state[\"comment_sentiments\"].sentiments]\n", + " state[\"overall_sentiment\"] = sum(sentiments) / len(sentiments) if sentiments else 0.0\n", + " return state\n", + "\n", + "def analyze_comment_sentiment(state: GraphState) -> GraphState:\n", + " prompt = PromptTemplate(template=template)\n", + " chain = prompt | structured_llm \n", + " \n", + " formatted_comments = \"\\n\\n\".join(\n", + " f\"Comment {i+1}:\\n{comment.body}\" for i, comment in enumerate(state[\"post\"].comments)\n", + " )\n", + "\n", + " result = chain.invoke({\n", + " \"reddit_post_title\": state[\"post\"].title,\n", + " \"reddit_post_body\": state[\"post\"].body,\n", + " \"comments\": formatted_comments\n", + " })\n", + "\n", + " try:\n", + " state[\"comment_sentiments\"] = result\n", + " except ValidationError as e:\n", + " print(\"Custom Validation Error in CommentSentiments:\", e)\n", + " raise\n", + "\n", + " return state" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Result Visualization\n", + "\n", + "Display results using rich:" + ] + }, + { + "cell_type": "code", + "execution_count": 133, + "metadata": {}, + "outputs": [], + "source": [ + "console = Console()\n", + "\n", + "def print_analysis(state: GraphState) -> GraphState:\n", + " console.print(\"\\n[bold underline]Reddit Comment Sentiment Analysis[/bold underline]\\n\")\n", + " console.print(f\"[bold]Post:[/bold] {state['post'].title}\\n\")\n", + " console.print(\"[bold]Individual Comments Analysis:[/bold]\")\n", + " \n", + " table = Table(show_header=True, header_style=\"bold white\")\n", + " table.add_column(\"Comment\", style=\"dim\", width=10)\n", + " table.add_column(\"Sentiment\")\n", + " table.add_column(\"Reason\", overflow=\"fold\")\n", + " \n", + " for i, (comment, sentiment) in enumerate(zip(state['post'].comments, state['comment_sentiments'].sentiments), 1):\n", + " score = sentiment.sentiment_score\n", + " if score > 0.1:\n", + " sentiment_display = f\"[green]๐Ÿ˜Š {score:+.1f}[/green]\"\n", + " elif score < -0.1:\n", + " sentiment_display = f\"[red]๐Ÿ˜ž {score:+.1f}[/red]\"\n", + " else:\n", + " sentiment_display = f\"[yellow]๐Ÿ˜ {score:+.1f}[/yellow]\"\n", + " \n", + " wrapped_reason = textwrap.fill(sentiment.sentiment_reason, width=50)\n", + " \n", + " table.add_row(str(i), sentiment_display, wrapped_reason)\n", + " \n", + " console.print(table)\n", + " \n", + " overall_score = state['overall_sentiment']\n", + " if overall_score > 0.1:\n", + " overall_display = f\"[green]๐Ÿ˜Š {overall_score:+.2f}[/green]\"\n", + " elif overall_score < -0.1:\n", + " overall_display = f\"[red]๐Ÿ˜ž {overall_score:+.2f}[/red]\"\n", + " else:\n", + " overall_display = f\"[yellow]๐Ÿ˜ {overall_score:+.2f}[/yellow]\"\n", + " \n", + " console.print(f\"\\n[bold]Overall Sentiment Score:[/bold] {overall_display}\")\n", + " \n", + " return state" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Graph Workflow\n", + "\n", + "Define and compile the workflow graph:" + ] + }, + { + "cell_type": "code", + "execution_count": 134, + "metadata": {}, + "outputs": [], + "source": [ + "def create_sentiment_graph():\n", + " workflow = Graph()\n", + " workflow.set_entry_point(\"fetch\")\n", + "\n", + " workflow.add_node(\"fetch\", fetch_reddit_content)\n", + " workflow.add_node(\"validate\", validate_reddit_content)\n", + " workflow.add_node(\"analyze\", analyze_comment_sentiment)\n", + " workflow.add_node(\"calculate\", calculate_overall_sentiment)\n", + " workflow.add_node(\"print\", print_analysis)\n", + "\n", + " workflow.add_edge(\"fetch\", \"validate\")\n", + " workflow.add_edge(\"analyze\", \"calculate\")\n", + " workflow.add_edge(\"calculate\", \"print\")\n", + " workflow.add_edge(\"print\", END)\n", + "\n", + " workflow.add_conditional_edges(\n", + " source=\"validate\",\n", + " path=determine_path,\n", + " path_map={\"analyze\": \"analyze\", END: END}\n", + " )\n", + "\n", + " return workflow.compile()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Graph Visualization\n", + "\n", + "Visualize the workflow graph:" + ] + }, + { + "cell_type": "code", + "execution_count": 135, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "graph = create_sentiment_graph()\n", + "\n", + "display(\n", + " IPImage(\n", + " graph.get_graph().draw_mermaid_png(\n", + " draw_method=MermaidDrawMethod.API,\n", + " )\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Executing the Workflow\n", + "\n", + "Run the graph with an initial state.\n", + "Feel free to change the Reddit post URL in the `initial_state` to analyze different posts comments." + ] + }, + { + "cell_type": "code", + "execution_count": 136, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n",
+       "Reddit Comment Sentiment Analysis\n",
+       "\n",
+       "
\n" + ], + "text/plain": [ + "\n", + "\u001b[1;4mReddit Comment Sentiment Analysis\u001b[0m\n", + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Post: Jenkins or Girhub Actions\n",
+       "\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1mPost:\u001b[0m Jenkins or Girhub Actions\n", + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Individual Comments Analysis:\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1mIndividual Comments Analysis:\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“\n",
+       "โ”ƒ Comment    โ”ƒ Sentiment โ”ƒ Reason                                             โ”ƒ\n",
+       "โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ\n",
+       "โ”‚ 1          โ”‚ ๐Ÿ˜Š +0.3   โ”‚ The comment suggests GitHub Actions over Jenkins,  โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ indicating a positive sentiment towards GitHub     โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ Actions and a slightly negative sentiment towards  โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ Jenkins. Key phrases: 'suggest GitHub actions',    โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ 'Jenkins... has been gradually fading'.            โ”‚\n",
+       "โ”‚ 2          โ”‚ ๐Ÿ˜Š +0.5   โ”‚ The comment strongly favors GitHub Actions and     โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ criticizes Jenkins for being outdated and having   โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ security risks. Key phrases: 'GitHub actions',     โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ 'Jenkins is just outdated', 'huge security risks'. โ”‚\n",
+       "โ”‚ 3          โ”‚ ๐Ÿ˜ +0.0   โ”‚ The comment is neutral and simply suggests GitLab  โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ without any emotional language or criticism. Key   โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ phrase: 'GitLab'.                                  โ”‚\n",
+       "โ”‚ 4          โ”‚ ๐Ÿ˜ +0.0   โ”‚ The comment is neutral and advises considering the โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ business case rather than popularity. Key phrases: โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ 'The right tech isn't always a popularity          โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ contest', 'make the business case'.                โ”‚\n",
+       "โ”‚ 5          โ”‚ ๐Ÿ˜Š +0.2   โ”‚ The comment favors GitLab for its extensibility    โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ and criticizes Jenkins for being too complex. It   โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ is neutral towards GitHub Actions. Key phrases:    โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ 'GitLab does', 'Jenkins is too complex'.           โ”‚\n",
+       "โ”‚ 6          โ”‚ ๐Ÿ˜ž -0.3   โ”‚ The comment criticizes both GitHub Actions and     โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ Jenkins, but more heavily criticizes Jenkins. Key  โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ phrases: 'GitHub Actions is far from perfect',     โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ 'Jenkins is an operational and security            โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ nightmare'.                                        โ”‚\n",
+       "โ”‚ 7          โ”‚ ๐Ÿ˜ +0.0   โ”‚ The comment suggests using GitHub/Gitea actions    โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ for building and Jenkins for deployment, showing a โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ mixed sentiment. Key phrases: 'github/gitea        โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ actions for building stuff', 'jenkins for          โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ deployment'.                                       โ”‚\n",
+       "โ”‚ 8          โ”‚ ๐Ÿ˜ +0.0   โ”‚ The comment is neutral and simply suggests GitHub  โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ Actions. Key phrase: 'GHA'.                        โ”‚\n",
+       "โ”‚ 9          โ”‚ ๐Ÿ˜ž -0.5   โ”‚ The comment is negative towards Jenkins without    โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ suggesting an alternative. Key phrase: 'Not        โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ Jenkins'.                                          โ”‚\n",
+       "โ”‚ 10         โ”‚ ๐Ÿ˜ +0.0   โ”‚ The comment is neutral and describes the use of    โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ both Jenkins and GitHub Actions during a           โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ transition period. Key phrases: 'legacy one is     โ”‚\n",
+       "โ”‚            โ”‚           โ”‚ still on Jenkins', 'new one is using GA'.          โ”‚\n",
+       "โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜\n",
+       "
\n" + ], + "text/plain": [ + "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“\n", + "โ”ƒ\u001b[1;37m \u001b[0m\u001b[1;37mComment \u001b[0m\u001b[1;37m \u001b[0mโ”ƒ\u001b[1;37m \u001b[0m\u001b[1;37mSentiment\u001b[0m\u001b[1;37m \u001b[0mโ”ƒ\u001b[1;37m \u001b[0m\u001b[1;37mReason \u001b[0m\u001b[1;37m \u001b[0mโ”ƒ\n", + "โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ\n", + "โ”‚\u001b[2m \u001b[0m\u001b[2m1 \u001b[0m\u001b[2m \u001b[0mโ”‚ \u001b[32m๐Ÿ˜Š +0.3\u001b[0m โ”‚ The comment suggests GitHub Actions over Jenkins, โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ indicating a positive sentiment towards GitHub โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ Actions and a slightly negative sentiment towards โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ Jenkins. Key phrases: 'suggest GitHub actions', โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ 'Jenkins... has been gradually fading'. โ”‚\n", + "โ”‚\u001b[2m \u001b[0m\u001b[2m2 \u001b[0m\u001b[2m \u001b[0mโ”‚ \u001b[32m๐Ÿ˜Š +0.5\u001b[0m โ”‚ The comment strongly favors GitHub Actions and โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ criticizes Jenkins for being outdated and having โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ security risks. Key phrases: 'GitHub actions', โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ 'Jenkins is just outdated', 'huge security risks'. โ”‚\n", + "โ”‚\u001b[2m \u001b[0m\u001b[2m3 \u001b[0m\u001b[2m \u001b[0mโ”‚ \u001b[33m๐Ÿ˜ +0.0\u001b[0m โ”‚ The comment is neutral and simply suggests GitLab โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ without any emotional language or criticism. Key โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ phrase: 'GitLab'. โ”‚\n", + "โ”‚\u001b[2m \u001b[0m\u001b[2m4 \u001b[0m\u001b[2m \u001b[0mโ”‚ \u001b[33m๐Ÿ˜ +0.0\u001b[0m โ”‚ The comment is neutral and advises considering the โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ business case rather than popularity. Key phrases: โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ 'The right tech isn't always a popularity โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ contest', 'make the business case'. โ”‚\n", + "โ”‚\u001b[2m \u001b[0m\u001b[2m5 \u001b[0m\u001b[2m \u001b[0mโ”‚ \u001b[32m๐Ÿ˜Š +0.2\u001b[0m โ”‚ The comment favors GitLab for its extensibility โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ and criticizes Jenkins for being too complex. It โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ is neutral towards GitHub Actions. Key phrases: โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ 'GitLab does', 'Jenkins is too complex'. โ”‚\n", + "โ”‚\u001b[2m \u001b[0m\u001b[2m6 \u001b[0m\u001b[2m \u001b[0mโ”‚ \u001b[31m๐Ÿ˜ž -0.3\u001b[0m โ”‚ The comment criticizes both GitHub Actions and โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ Jenkins, but more heavily criticizes Jenkins. Key โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ phrases: 'GitHub Actions is far from perfect', โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ 'Jenkins is an operational and security โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ nightmare'. โ”‚\n", + "โ”‚\u001b[2m \u001b[0m\u001b[2m7 \u001b[0m\u001b[2m \u001b[0mโ”‚ \u001b[33m๐Ÿ˜ +0.0\u001b[0m โ”‚ The comment suggests using GitHub/Gitea actions โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ for building and Jenkins for deployment, showing a โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ mixed sentiment. Key phrases: 'github/gitea โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ actions for building stuff', 'jenkins for โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ deployment'. โ”‚\n", + "โ”‚\u001b[2m \u001b[0m\u001b[2m8 \u001b[0m\u001b[2m \u001b[0mโ”‚ \u001b[33m๐Ÿ˜ +0.0\u001b[0m โ”‚ The comment is neutral and simply suggests GitHub โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ Actions. Key phrase: 'GHA'. โ”‚\n", + "โ”‚\u001b[2m \u001b[0m\u001b[2m9 \u001b[0m\u001b[2m \u001b[0mโ”‚ \u001b[31m๐Ÿ˜ž -0.5\u001b[0m โ”‚ The comment is negative towards Jenkins without โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ suggesting an alternative. Key phrase: 'Not โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ Jenkins'. โ”‚\n", + "โ”‚\u001b[2m \u001b[0m\u001b[2m10 \u001b[0m\u001b[2m \u001b[0mโ”‚ \u001b[33m๐Ÿ˜ +0.0\u001b[0m โ”‚ The comment is neutral and describes the use of โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ both Jenkins and GitHub Actions during a โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ transition period. Key phrases: 'legacy one is โ”‚\n", + "โ”‚\u001b[2m \u001b[0mโ”‚ โ”‚ still on Jenkins', 'new one is using GA'. โ”‚\n", + "โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n",
+       "Overall Sentiment Score: ๐Ÿ˜ +0.02\n",
+       "
\n" + ], + "text/plain": [ + "\n", + "\u001b[1mOverall Sentiment Score:\u001b[0m \u001b[33m๐Ÿ˜ +\u001b[0m\u001b[1;33m0.02\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "initial_state = GraphState(\n", + " url=\"https://www.reddit.com/r/devops/comments/1hir6a5/jenkins_or_girhub_actions/\",\n", + " post=RedditPost(\n", + " title=\"\",\n", + " body=\"\",\n", + " comments=[]\n", + " ),\n", + " comment_sentiments=[],\n", + " overall_sentiment=0.0,\n", + " is_valid=False\n", + ")\n", + "\n", + "result = graph.invoke(initial_state)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Summary\n", + "\n", + "This notebook integrates LangGraph and MistralAI's LLM to perform sentiment analysis on Reddit comments.\n", + "It fetches posts, analyzes sentiments, aggregates results, and visualizes the workflow, providing insights into community sentiments effectively and efficiently." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}