diff --git a/community/README.md b/community/README.md index c4c89f53..210e1657 100644 --- a/community/README.md +++ b/community/README.md @@ -23,6 +23,10 @@ Community examples are sample code and deployments for RAG pipelines that are no ## Inventory +* [NVIDIA Data Analysis Agent](./data-analysis-agent/) + + This example demonstrates an interactive, agentic data analysis application that leverages NVIDIA Llama-3.1-Nemotron-Ultra-253B-v1 for advanced reasoning and data exploration. Users can upload CSV files, ask questions in natural language, and receive automated visualizations with clear, step-by-step reasoning. The implementation features a modular agent architecture for data insight, code generation, execution, and transparent reasoning. + * [NVIDIA RAG in 5 minutes](./5_mins_rag_no_gpu/) This is a simple standalone implementation showing rag pipeline using Nvidia API Catalog models. It uses a simple Streamlit UI and one file implementation of a minimalistic RAG pipeline. diff --git a/community/data-analysis-agent/README.md b/community/data-analysis-agent/README.md new file mode 100644 index 00000000..1c3515a2 --- /dev/null +++ b/community/data-analysis-agent/README.md @@ -0,0 +1,99 @@ +# Data Analysis Agent + +An interactive, agentic data analysis application that leverages advanced LLM reasoning to help users explore, visualize, and understand their data using NVIDIA Llama-3.1-Nemotron-Ultra-253B-v1. + +## Overview + +This repository contains a Streamlit application that demonstrates a complete workflow for data analysis: +1. **Data Upload**: Upload CSV files for analysis +2. **Natural Language Queries**: Ask questions about your data in plain English +3. **Automated Visualization**: Generate relevant plots and charts +4. **Transparent Reasoning**: Get detailed explanations of the analysis process + +The implementation leverages the powerful Llama-3.1-Nemotron-Ultra-253B-v1 model through NVIDIA's API, enabling sophisticated data analysis and reasoning. + +Learn more about the model [here](https://developer.nvidia.com/blog/build-enterprise-ai-agents-with-advanced-open-nvidia-llama-nemotron-reasoning-models/). + +## Features + +- **Agentic Architecture**: Modular agents for data insight, code generation, execution, and reasoning +- **Natural Language Queries**: Ask questions about your data—no coding required +- **Automated Visualization**: Instantly generate and display relevant plots +- **Transparent Reasoning**: Get clear, LLM-generated explanations for every result +- **Powered by NVIDIA Llama-3.1-Nemotron-Ultra-253B-v1**: State-of-the-art reasoning and interpretability + +![Workflow](./assets/workflow.png) + +## Requirements + +- Python 3.10+ +- Streamlit +- NVIDIA API Key (see [Installation](#installation) section for setup instructions) +- Required Python packages: + - pandas + - matplotlib + - streamlit + - requests + +## Installation + +1. Clone this repository: + ```bash + git clone https://github.com/NVIDIA/GenerativeAIExamples.git + cd GenerativeAIExamples/community/data-analysis-agent + ``` + +2. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +3. Set up your NVIDIA API key: + - Sign up or log in at [NVIDIA Build](https://build.nvidia.com/nvidia/llama-3_1-nemotron-ultra-253b-v1?integrate_nim=true&hosted_api=true&modal=integrate-nim) + - Generate an API key + - Set the API key in your environment: + ```bash + export NVIDIA_API_KEY=your_nvidia_api_key_here + ``` + - Or add it to your `.env` file if you use one + +## Usage + +1. Run the Streamlit app: + ```bash + streamlit run data_analysis.py + ``` + +2. Download example dataset (optional): + ```bash + wget https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv + ``` + +3. Use the application: + - Upload a CSV file (e.g., the Titanic dataset) + - Ask questions in natural language + - View results, visualizations, and detailed reasoning + +## Example + +![App Demo](./assets/data_analysis_agent_demo.png) + +## Model Details + +The Llama-3.1-Nemotron-Ultra-253B-v1 model used in this project has the following specifications: +- **Parameters**: 253B +- **Features**: Advanced reasoning capabilities +- **Use Cases**: Complex data analysis, multi-agent systems +- **Enterprise Ready**: Optimized for production deployment + +## Acknowledgments + +- [NVIDIA Llama-3.1-Nemotron-Ultra-253B-v1](https://build.nvidia.com/nvidia/llama-3_1-nemotron-ultra-253b-v1) +- [Streamlit](https://streamlit.io/) +- [Pandas](https://pandas.pydata.org/) +- [Matplotlib](https://matplotlib.org/) + + +## Contributing + +Contributions are welcome! Please open an issue or submit a pull request. diff --git a/community/data-analysis-agent/assets/data_analysis_agent_demo.png b/community/data-analysis-agent/assets/data_analysis_agent_demo.png new file mode 100644 index 00000000..a753e8c0 Binary files /dev/null and b/community/data-analysis-agent/assets/data_analysis_agent_demo.png differ diff --git a/community/data-analysis-agent/assets/workflow.png b/community/data-analysis-agent/assets/workflow.png new file mode 100644 index 00000000..63c112e8 Binary files /dev/null and b/community/data-analysis-agent/assets/workflow.png differ diff --git a/community/data-analysis-agent/data_analysis_agent.py b/community/data-analysis-agent/data_analysis_agent.py new file mode 100644 index 00000000..128e2645 --- /dev/null +++ b/community/data-analysis-agent/data_analysis_agent.py @@ -0,0 +1,345 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os, io, re +import pandas as pd +import streamlit as st +from openai import OpenAI +import matplotlib.pyplot as plt +from typing import List, Dict, Any, Tuple + +# === Configuration === +api_key = os.environ.get("NVIDIA_API_KEY") + +client = OpenAI( + base_url="https://integrate.api.nvidia.com/v1", + api_key=api_key +) + +# ------------------ QueryUnderstandingTool --------------------------- +def QueryUnderstandingTool(query: str) -> bool: + """Return True if the query seems to request a visualisation based on keywords.""" + # Use LLM to understand intent instead of keyword matching + messages = [ + {"role": "system", "content": "detailed thinking off. You are an assistant that determines if a query is requesting a data visualization. Respond with only 'true' if the query is asking for a plot, chart, graph, or any visual representation of data. Otherwise, respond with 'false'."}, + {"role": "user", "content": query} + ] + + response = client.chat.completions.create( + model="nvidia/llama-3.1-nemotron-ultra-253b-v1", + messages=messages, + temperature=0.1, + max_tokens=5 # We only need a short response + ) + + # Extract the response and convert to boolean + intent_response = response.choices[0].message.content.strip().lower() + return intent_response == "true" + +# === CodeGeneration TOOLS ============================================ + +# ------------------ PlotCodeGeneratorTool --------------------------- +def PlotCodeGeneratorTool(cols: List[str], query: str) -> str: + """Generate a prompt for the LLM to write pandas+matplotlib code for a plot based on the query and columns.""" + return f""" + Given DataFrame `df` with columns: {', '.join(cols)} + Write Python code using pandas **and matplotlib** (as plt) to answer: + "{query}" + + Rules + ----- + 1. Use pandas for data manipulation and matplotlib.pyplot (as plt) for plotting. + 2. Assign the final result (DataFrame, Series, scalar *or* matplotlib Figure) to a variable named `result`. + 3. Create only ONE relevant plot. Set `figsize=(6,4)`, add title/labels. + 4. Return your answer inside a single markdown fence that starts with ```python and ends with ```. + """ + +# ------------------ CodeWritingTool --------------------------------- +def CodeWritingTool(cols: List[str], query: str) -> str: + """Generate a prompt for the LLM to write pandas-only code for a data query (no plotting).""" + return f""" + Given DataFrame `df` with columns: {', '.join(cols)} + Write Python code (pandas **only**, no plotting) to answer: + "{query}" + + Rules + ----- + 1. Use pandas operations on `df` only. + 2. Assign the final result to `result`. + 3. Wrap the snippet in a single ```python code fence (no extra prose). + """ + +# === CodeGenerationAgent ============================================== + +def CodeGenerationAgent(query: str, df: pd.DataFrame): + """Selects the appropriate code generation tool and gets code from the LLM for the user's query.""" + should_plot = QueryUnderstandingTool(query) + prompt = PlotCodeGeneratorTool(df.columns.tolist(), query) if should_plot else CodeWritingTool(df.columns.tolist(), query) + + messages = [ + {"role": "system", "content": "detailed thinking off. You are a Python data-analysis expert who writes clean, efficient code. Solve the given problem with optimal pandas operations. Be concise and focused. Your response must contain ONLY a properly-closed ```python code block with no explanations before or after. Ensure your solution is correct, handles edge cases, and follows best practices for data analysis."}, + {"role": "user", "content": prompt} + ] + + response = client.chat.completions.create( + model="nvidia/llama-3.1-nemotron-ultra-253b-v1", + messages=messages, + temperature=0.2, + max_tokens=1024 + ) + + full_response = response.choices[0].message.content + code = extract_first_code_block(full_response) + return code, should_plot, "" + +# === ExecutionAgent ==================================================== + +def ExecutionAgent(code: str, df: pd.DataFrame, should_plot: bool): + """Executes the generated code in a controlled environment and returns the result or error message.""" + env = {"pd": pd, "df": df} + if should_plot: + plt.rcParams["figure.dpi"] = 100 # Set default DPI for all figures + env["plt"] = plt + env["io"] = io + try: + exec(code, {}, env) + return env.get("result", None) + except Exception as exc: + return f"Error executing code: {exc}" + +# === ReasoningCurator TOOL ========================================= +def ReasoningCurator(query: str, result: Any) -> str: + """Builds and returns the LLM prompt for reasoning about the result.""" + is_error = isinstance(result, str) and result.startswith("Error executing code") + is_plot = isinstance(result, (plt.Figure, plt.Axes)) + + if is_error: + desc = result + elif is_plot: + title = "" + if isinstance(result, plt.Figure): + title = result._suptitle.get_text() if result._suptitle else "" + elif isinstance(result, plt.Axes): + title = result.get_title() + desc = f"[Plot Object: {title or 'Chart'}]" + else: + desc = str(result)[:300] + + if is_plot: + prompt = f''' + The user asked: "{query}". + Below is a description of the plot result: + {desc} + Explain in 2–3 concise sentences what the chart shows (no code talk).''' + else: + prompt = f''' + The user asked: "{query}". + The result value is: {desc} + Explain in 2–3 concise sentences what this tells about the data (no mention of charts).''' + return prompt + +# === ReasoningAgent (streaming) ========================================= +def ReasoningAgent(query: str, result: Any): + """Streams the LLM's reasoning about the result (plot or value) and extracts model 'thinking' and final explanation.""" + prompt = ReasoningCurator(query, result) + is_error = isinstance(result, str) and result.startswith("Error executing code") + is_plot = isinstance(result, (plt.Figure, plt.Axes)) + + # Streaming LLM call + response = client.chat.completions.create( + model="nvidia/llama-3.1-nemotron-ultra-253b-v1", + messages=[ + {"role": "system", "content": "detailed thinking on. You are an insightful data analyst."}, + {"role": "user", "content": prompt} + ], + temperature=0.2, + max_tokens=1024, + stream=True + ) + + # Stream and display thinking + thinking_placeholder = st.empty() + full_response = "" + thinking_content = "" + in_think = False + + for chunk in response: + if chunk.choices[0].delta.content is not None: + token = chunk.choices[0].delta.content + full_response += token + + # Simple state machine to extract ... as it streams + if "" in token: + in_think = True + token = token.split("", 1)[1] + if "" in token: + token = token.split("", 1)[0] + in_think = False + if in_think or ("" in full_response and not "" in full_response): + thinking_content += token + thinking_placeholder.markdown( + f'
🤔 Model Thinking
{thinking_content}
', + unsafe_allow_html=True + ) + + # After streaming, extract final reasoning (outside ...) + cleaned = re.sub(r".*?", "", full_response, flags=re.DOTALL).strip() + return thinking_content, cleaned + +# === DataFrameSummary TOOL (pandas only) ========================================= +def DataFrameSummaryTool(df: pd.DataFrame) -> str: + """Generate a summary prompt string for the LLM based on the DataFrame.""" + prompt = f""" + Given a dataset with {len(df)} rows and {len(df.columns)} columns: + Columns: {', '.join(df.columns)} + Data types: {df.dtypes.to_dict()} + Missing values: {df.isnull().sum().to_dict()} + + Provide: + 1. A brief description of what this dataset contains + 2. 3-4 possible data analysis questions that could be explored + Keep it concise and focused.""" + return prompt + +# === DataInsightAgent (upload-time only) =============================== + +def DataInsightAgent(df: pd.DataFrame) -> str: + """Uses the LLM to generate a brief summary and possible questions for the uploaded dataset.""" + prompt = DataFrameSummaryTool(df) + try: + response = client.chat.completions.create( + model="nvidia/llama-3.1-nemotron-ultra-253b-v1", + messages=[ + {"role": "system", "content": "detailed thinking off. You are a data analyst providing brief, focused insights."}, + {"role": "user", "content": prompt} + ], + temperature=0.2, + max_tokens=512 + ) + return response.choices[0].message.content + except Exception as exc: + return f"Error generating dataset insights: {exc}" + +# === Helpers =========================================================== + +def extract_first_code_block(text: str) -> str: + """Extracts the first Python code block from a markdown-formatted string.""" + start = text.find("```python") + if start == -1: + return "" + start += len("```python") + end = text.find("```", start) + if end == -1: + return "" + return text[start:end].strip() + +# === Main Streamlit App =============================================== + +def main(): + st.set_page_config(layout="wide") + if "plots" not in st.session_state: + st.session_state.plots = [] + + left, right = st.columns([3,7]) + + with left: + st.header("Data Analysis Agent") + st.markdown("Powered by NVIDIA Llama-3.1-Nemotron-Ultra-253B-v1", unsafe_allow_html=True) + file = st.file_uploader("Choose CSV", type=["csv"]) + if file: + if ("df" not in st.session_state) or (st.session_state.get("current_file") != file.name): + st.session_state.df = pd.read_csv(file) + st.session_state.current_file = file.name + st.session_state.messages = [] + with st.spinner("Generating dataset insights …"): + st.session_state.insights = DataInsightAgent(st.session_state.df) + st.dataframe(st.session_state.df.head()) + st.markdown("### Dataset Insights") + st.markdown(st.session_state.insights) + else: + st.info("Upload a CSV to begin chatting with your data.") + + with right: + st.header("Chat with your data") + if "messages" not in st.session_state: + st.session_state.messages = [] + + chat_container = st.container() + with chat_container: + for msg in st.session_state.messages: + with st.chat_message(msg["role"]): + st.markdown(msg["content"], unsafe_allow_html=True) + if msg.get("plot_index") is not None: + idx = msg["plot_index"] + if 0 <= idx < len(st.session_state.plots): + # Display plot at fixed size + st.pyplot(st.session_state.plots[idx], use_container_width=False) + + if file: # only allow chat after upload + if user_q := st.chat_input("Ask about your data…"): + st.session_state.messages.append({"role": "user", "content": user_q}) + with st.spinner("Working …"): + code, should_plot_flag, code_thinking = CodeGenerationAgent(user_q, st.session_state.df) + result_obj = ExecutionAgent(code, st.session_state.df, should_plot_flag) + raw_thinking, reasoning_txt = ReasoningAgent(user_q, result_obj) + reasoning_txt = reasoning_txt.replace("`", "") + + # Build assistant response + is_plot = isinstance(result_obj, (plt.Figure, plt.Axes)) + plot_idx = None + if is_plot: + fig = result_obj.figure if isinstance(result_obj, plt.Axes) else result_obj + st.session_state.plots.append(fig) + plot_idx = len(st.session_state.plots) - 1 + header = "Here is the visualization you requested:" + elif isinstance(result_obj, (pd.DataFrame, pd.Series)): + header = f"Result: {len(result_obj)} rows" if isinstance(result_obj, pd.DataFrame) else "Result series" + else: + header = f"Result: {result_obj}" + + # Show only reasoning thinking in Model Thinking (collapsed by default) + thinking_html = "" + if raw_thinking: + thinking_html = ( + '
' + '🧠 Reasoning' + f'
{raw_thinking}
' + '
' + ) + + # Show model explanation directly + explanation_html = reasoning_txt + + # Code accordion with proper HTML
 syntax highlighting
+                code_html = (
+                    '
' + 'View code' + '
'
+                    f'{code}'
+                    '
' + '
' + ) + # Combine thinking, explanation, and code accordion + assistant_msg = f"{thinking_html}{explanation_html}\n\n{code_html}" + + st.session_state.messages.append({ + "role": "assistant", + "content": assistant_msg, + "plot_index": plot_idx + }) + st.rerun() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/community/data-analysis-agent/requirements.txt b/community/data-analysis-agent/requirements.txt new file mode 100644 index 00000000..4e77d302 --- /dev/null +++ b/community/data-analysis-agent/requirements.txt @@ -0,0 +1,6 @@ +streamlit>=1.32.0 +pandas>=2.2.0 +matplotlib>=3.8.0 +seaborn>=0.13.0 +openai>=1.12.0 +watchdog>=3.0.0 \ No newline at end of file