diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9d866e3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/1_phidata_finance_agent/1_simple_groq_agent.py b/1_phidata_finance_agent/1_simple_groq_agent.py index 286a207..fd8b566 100644 --- a/1_phidata_finance_agent/1_simple_groq_agent.py +++ b/1_phidata_finance_agent/1_simple_groq_agent.py @@ -1,12 +1,22 @@ +import os +# Add your GROQ_API_KEY and optionally GROQ_MODEL_ID to a .env file or set them as environment variables. from phi.agent import Agent from phi.model.groq import Groq from dotenv import load_dotenv load_dotenv() +if not os.getenv("GROQ_API_KEY"): + print("Warning: GROQ_API_KEY not found. Please set it in your .env file or as an environment variable.") + +# Create a simple agent with a Groq model agent = Agent( - model=Groq(id="llama-3.3-70b-versatile") + model=Groq(id=os.getenv("GROQ_MODEL_ID", "llama-3.3-70b-versatile")) + # No specific instructions or tools are provided, so it will use the model's default capabilities. ) - -agent.print_response("Share a 2 sentence love story between dosa and samosa") \ No newline at end of file +# Example usage of the agent +try: + agent.print_response("Share a 2 sentence love story between dosa and samosa") +except Exception as e: + print(f"An error occurred: {e}") \ No newline at end of file diff --git a/1_phidata_finance_agent/2_finance_agent_llama.py b/1_phidata_finance_agent/2_finance_agent_llama.py index 1c88d8f..f89c8da 100644 --- a/1_phidata_finance_agent/2_finance_agent_llama.py +++ b/1_phidata_finance_agent/2_finance_agent_llama.py @@ -1,3 +1,5 @@ +import os +# Add your GROQ_API_KEY and optionally GROQ_MODEL_ID to a .env file or set them as environment variables. """Run `pip install yfinance` to install dependencies.""" from phi.agent import Agent @@ -6,6 +8,8 @@ from dotenv import load_dotenv load_dotenv() +if not os.getenv("GROQ_API_KEY"): + print("Warning: GROQ_API_KEY not found. Please set it in your .env file or as an environment variable.") def get_company_symbol(company: str) -> str: @@ -17,6 +21,8 @@ def get_company_symbol(company: str) -> str: Returns: str: The symbol for the company. """ + # This is a simplified lookup for tutorial purposes. + # A real application should use a dedicated API or a more comprehensive database for symbol lookups. symbols = { "Phidata": "MSFT", "Infosys": "INFY", @@ -25,22 +31,32 @@ def get_company_symbol(company: str) -> str: "Microsoft": "MSFT", "Amazon": "AMZN", "Google": "GOOGL", + "Netflix": "NFLX", + "Nvidia": "NVDA", } return symbols.get(company, "Unknown") +# YFinanceTools provides the agent with capabilities to fetch stock prices, analyst recommendations, and company fundamentals. agent = Agent( - model=Groq(id="llama-3.3-70b-versatile"), + model=Groq(id=os.getenv("GROQ_MODEL_ID", "llama-3.3-70b-versatile")), tools=[YFinanceTools(stock_price=True, analyst_recommendations=True, stock_fundamentals=True), get_company_symbol], + # Instructions guide the agent on how to behave and use its tools. instructions=[ "Use tables to display data.", "If you need to find the symbol for a company, use the get_company_symbol tool.", ], - show_tool_calls=True, - markdown=True, - debug_mode=True, + show_tool_calls=True, # Displays tool calls and their responses. + markdown=True, # Outputs responses in Markdown format. + debug_mode=True, # Enables detailed logging for debugging. ) -agent.print_response( - "Summarize and compare analyst recommendations and fundamentals for TSLA and MSFT. Show in tables.", stream=True -) \ No newline at end of file +# Example usage of the finance agent +# The agent will use YFinanceTools to get financial data and get_company_symbol to find stock symbols. +# It will then use the Groq model to process this information and generate a response. +try: + agent.print_response( + "Summarize and compare analyst recommendations and fundamentals for TSLA and MSFT. Show in tables.", stream=True + ) +except Exception as e: + print(f"An error occurred: {e}") diff --git a/1_phidata_finance_agent/3_agent_teams_openai.py b/1_phidata_finance_agent/3_agent_teams_openai.py index 399f80f..d7d8e51 100644 --- a/1_phidata_finance_agent/3_agent_teams_openai.py +++ b/1_phidata_finance_agent/3_agent_teams_openai.py @@ -1,3 +1,6 @@ +import os +# Add your OPENAI_API_KEY (and optionally OPENAI_MODEL_ID) and/or GROQ_API_KEY (and optionally GROQ_MODEL_ID) +# to a .env file or set them as environment variables, depending on the model used. from phi.agent import Agent from phi.model.openai import OpenAIChat from phi.model.groq import Groq @@ -6,35 +9,59 @@ from dotenv import load_dotenv load_dotenv() +# Default model is OpenAI, so check for OPENAI_API_KEY first. +if not os.getenv("OPENAI_API_KEY"): + print("Warning: OPENAI_API_KEY not found. Please set it in your .env file or as an environment variable if you plan to use OpenAI models.") +if not os.getenv("GROQ_API_KEY"): + print("Warning: GROQ_API_KEY not found. Please set it in your .env file or as an environment variable if you plan to use Groq models.") +# --- Choosing between OpenAI and Groq Models --- +# This script defaults to using OpenAI models (gpt-4o specified by OPENAI_MODEL_ID). +# To use Groq models (e.g., llama-3.3-70b-versatile specified by GROQ_MODEL_ID): +# 1. Comment out the `model=OpenAIChat(...)` line for each agent definition below. +# 2. Uncomment the `model=Groq(...)` line for each agent definition. +# +# Ensure the relevant API keys (OPENAI_API_KEY, GROQ_API_KEY) and any chosen +# model IDs (OPENAI_MODEL_ID, GROQ_MODEL_ID) are set in your environment, +# typically in a .env file at the root of your project. + +# Web Agent: Specialist agent with web search capabilities using DuckDuckGo. web_agent = Agent( name="Web Agent", - # model=Groq(id="llama-3.3-70b-versatile"), - model=OpenAIChat(id="gpt-4o"), + # model=Groq(id=os.getenv("GROQ_MODEL_ID", "llama-3.3-70b-versatile")), + model=OpenAIChat(id=os.getenv("OPENAI_MODEL_ID", "gpt-4o")), tools=[DuckDuckGo()], instructions=["Always include sources"], - show_tool_calls=True, - markdown=True + show_tool_calls=True, # Displays tool calls and their responses. + markdown=True # Outputs responses in Markdown format. ) +# Finance Agent: Specialist agent with financial data fetching capabilities using YFinanceTools. finance_agent = Agent( name="Finance Agent", role="Get financial data", - # model=Groq(id="llama-3.3-70b-versatile"), - model=OpenAIChat(id="gpt-4o"), + # model=Groq(id=os.getenv("GROQ_MODEL_ID", "llama-3.3-70b-versatile")), + model=OpenAIChat(id=os.getenv("OPENAI_MODEL_ID", "gpt-4o")), tools=[YFinanceTools(stock_price=True, analyst_recommendations=True, company_info=True)], instructions=["Use tables to display data"], - show_tool_calls=True, - markdown=True, + show_tool_calls=True, # Displays tool calls and their responses. + markdown=True, # Outputs responses in Markdown format. ) +# Agent Team: Supervisor agent that delegates tasks to specialist agents (Web Agent, Finance Agent) +# based on the nature of the prompt. It coordinates the work of the team. agent_team = Agent( - # model=Groq(id="llama-3.3-70b-versatile"), - model=OpenAIChat(id="gpt-4o"), + # model=Groq(id=os.getenv("GROQ_MODEL_ID", "llama-3.3-70b-versatile")), + model=OpenAIChat(id=os.getenv("OPENAI_MODEL_ID", "gpt-4o")), team=[web_agent, finance_agent], instructions=["Always include sources", "Use tables to display data"], - show_tool_calls=True, - markdown=True, + show_tool_calls=True, # Displays tool calls and their responses for the team. + markdown=True, # Outputs the final response in Markdown format. ) -agent_team.print_response("Summarize analyst recommendations and share the latest news for NVDA", stream=True) +# Example usage of the agent team +# The team will delegate tasks to the Web Agent for news and Finance Agent for financial data. +try: + agent_team.print_response("Summarize analyst recommendations and share the latest news for NVDA", stream=True) +except Exception as e: + print(f"An error occurred: {e}") diff --git a/1_phidata_finance_agent/requirements.txt b/1_phidata_finance_agent/requirements.txt new file mode 100644 index 0000000..94dd035 --- /dev/null +++ b/1_phidata_finance_agent/requirements.txt @@ -0,0 +1,6 @@ +phi-ai +python-dotenv +groq +openai +yfinance +duckduckgo-search diff --git a/2_mcp_leave_management/README.md b/2_mcp_leave_management/README.md index 4b10964..0f3683d 100644 --- a/2_mcp_leave_management/README.md +++ b/2_mcp_leave_management/README.md @@ -13,4 +13,9 @@ is for MCP server that interacts with mock leave database and responds to MCP cl 8. Kill any running instance of Claude from Task Manager. Restart Claude Desktop 9. In Claude desktop, now you will see tools from this server +# Database Information +- This server uses an SQLite database named `leave_management.db`. +- When you run the server (e.g., using `python main.py` or via the MCP client interaction if it's started by the client environment), this database file will be automatically created in the `2_mcp_leave_management/` directory if it doesn't already exist. +- The database is also automatically initialized with sample employee data and leave history upon its first creation, allowing you to test the tools immediately. + @All rights reserved. Codebasics Inc. LearnerX Pvt Ltd. diff --git a/2_mcp_leave_management/database.py b/2_mcp_leave_management/database.py new file mode 100644 index 0000000..91d7812 --- /dev/null +++ b/2_mcp_leave_management/database.py @@ -0,0 +1,150 @@ +""" +Handles SQLite database operations for the Leave Management System. + +This module includes functions for: +- Establishing database connections. +- Creating necessary tables (`employees`, `leave_history`) if they don't exist. +- Initializing the database with sample employee data and leave history. +- Retrieving employee leave balance and history. +- Updating employee leave balances and adding new leave entries. + +The database file is named `leave_management.db` and is located in the +`2_mcp_leave_management` directory. +""" +import sqlite3 +from typing import List, Dict, Any, Tuple + + +DATABASE_NAME = "2_mcp_leave_management/leave_management.db" + + +def get_db_connection() -> sqlite3.Connection: + """Establishes a connection to the SQLite database.""" + conn = sqlite3.connect(DATABASE_NAME) + conn.row_factory = sqlite3.Row # Access columns by name + return conn + +def create_tables_if_not_exist(): + """Creates the necessary tables if they don't already exist.""" + # Ensures that the employees and leave_history tables are created before any operations. + conn = get_db_connection() + cursor = conn.cursor() + + # Employee table: stores employee ID and their current leave balance + cursor.execute(''' + CREATE TABLE IF NOT EXISTS employees ( + employee_id TEXT PRIMARY KEY, + balance INTEGER NOT NULL + ) + ''') + + # Leave history table: stores individual leave dates for each employee + cursor.execute(''' + CREATE TABLE IF NOT EXISTS leave_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + employee_id TEXT NOT NULL, + leave_date TEXT NOT NULL, + FOREIGN KEY (employee_id) REFERENCES employees (employee_id) + ) + ''') + conn.commit() + conn.close() + +def initialize_database_with_sample_data(): + """Populates the database with initial sample data if it's empty.""" + # Idempotent function: only adds data if the 'employees' table is currently empty. + conn = get_db_connection() + cursor = conn.cursor() + + # Check if employees table is empty + cursor.execute("SELECT COUNT(*) FROM employees") + if cursor.fetchone()[0] == 0: + sample_employees: List[Tuple[str, int]] = [ + ("E001", 18), + ("E002", 20) + ] + cursor.executemany("INSERT INTO employees (employee_id, balance) VALUES (?, ?)", sample_employees) + + sample_leave_history: List[Tuple[str, str]] = [ + ("E001", "2024-12-25"), + ("E001", "2025-01-01") + ] + cursor.executemany("INSERT INTO leave_history (employee_id, leave_date) VALUES (?, ?)", sample_leave_history) + + conn.commit() + conn.close() + +def get_employee_data(employee_id: str) -> Dict[str, Any]: + """Retrieves employee balance and history. Returns None if employee_id is not found.""" + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute("SELECT balance FROM employees WHERE employee_id = ?", (employee_id,)) + employee_row = cursor.fetchone() + + if not employee_row: + conn.close() + return None + + balance = employee_row["balance"] + + cursor.execute("SELECT leave_date FROM leave_history WHERE employee_id = ? ORDER BY leave_date", (employee_id,)) + history_rows = cursor.fetchall() + history = [row["leave_date"] for row in history_rows] + + conn.close() + return {"balance": balance, "history": history} + +def update_employee_leave(employee_id: str, new_balance: int, leave_dates_to_add: List[str]) -> bool: + """ + Updates an employee's leave balance and adds new leave dates to their history. + This operation is transactional: either all changes are committed or none are. + Returns True if successful, False otherwise. + """ + conn = get_db_connection() + cursor = conn.cursor() + + try: + # Update employee's balance + cursor.execute("UPDATE employees SET balance = ? WHERE employee_id = ?", (new_balance, employee_id)) + if cursor.rowcount == 0: # No rows updated means employee_id was not found + conn.close() + return False # Employee not found, or no update was needed (though balance change implies it was) + + # Add new leave dates to history + if leave_dates_to_add: + history_data: List[Tuple[str, str]] = [(employee_id, date_str) for date_str in leave_dates_to_add] + cursor.executemany("INSERT INTO leave_history (employee_id, leave_date) VALUES (?, ?)", history_data) + + conn.commit() # Commit transaction + return True + except sqlite3.Error as e: # Catch any SQLite-related errors + conn.rollback() # Rollback transaction in case of any error during DB operations + # Optionally, log the error e here + return False + finally: + conn.close() + +# Initialize tables and data when this module is loaded +create_tables_if_not_exist() +initialize_database_with_sample_data() + +if __name__ == '__main__': + # Example usage for testing the database module directly + print("Database module initialized.") + print("Data for E001:", get_employee_data("E001")) + print("Data for E002:", get_employee_data("E002")) + print("Data for E003 (non-existent):", get_employee_data("E003")) + + # Example apply leave + # print("\nAttempting to apply leave for E002...") + # if get_employee_data("E002")['balance'] >= 2: + # new_dates = ["2025-03-10", "2025-03-11"] + # current_balance = get_employee_data("E002")['balance'] + # if update_employee_leave("E002", current_balance - len(new_dates), new_dates): + # print("Leave applied successfully for E002.") + # print("Updated data for E002:", get_employee_data("E002")) + # else: + # print("Failed to apply leave for E002.") + # else: + # print("E002 has insufficient balance for this example.") diff --git a/2_mcp_leave_management/main.py b/2_mcp_leave_management/main.py index 0c676fa..18ce5c6 100644 --- a/2_mcp_leave_management/main.py +++ b/2_mcp_leave_management/main.py @@ -1,61 +1,131 @@ +""" +Main MCP server file for the Leave Management Tutorial. + +This script defines an MCP (My Computational Platform) server with tools for: +- Checking employee leave balances. +- Applying for leave, with validation for dates and availability. +- Retrieving employee leave history. + +It integrates with `database.py` to persist leave data in an SQLite database. +The server is an instance of FastMCP, named "LeaveManager". +""" from mcp.server.fastmcp import FastMCP from typing import List +from . import database # Use relative import for modules within the same package +import datetime +import re -# In-memory mock database with 20 leave days to start -employee_leaves = { - "E001": {"balance": 18, "history": ["2024-12-25", "2025-01-01"]}, - "E002": {"balance": 20, "history": []} -} -# Create MCP server +# Create MCP server instance, naming it "LeaveManager" mcp = FastMCP("LeaveManager") -# Tool: Check Leave Balance + +# --- Tool Definitions --- + @mcp.tool() def get_leave_balance(employee_id: str) -> str: - """Check how many leave days are left for the employee""" - data = employee_leaves.get(employee_id) + """ + Checks the remaining leave balance for a given employee ID. + Example: "How many leave days does E001 have?" + """ + data = database.get_employee_data(employee_id) if data: - return f"{employee_id} has {data['balance']} leave days remaining." + return f"Employee {employee_id} has {data['balance']} leave days remaining." return "Employee ID not found." # Tool: Apply for Leave with specific dates @mcp.tool() def apply_leave(employee_id: str, leave_dates: List[str]) -> str: """ - Apply leave for specific dates (e.g., ["2025-04-17", "2025-05-01"]) + Apply leave for an employee for one or more specific dates. + Dates should be provided in "YYYY-MM-DD" format (e.g., ["2025-04-17", "2025-05-01"]). + The tool validates date formats, checks if dates are in the past, + verifies if dates are already booked, and ensures sufficient leave balance. + Example: "Apply leave for E001 on 2025-06-10 and 2025-06-11" """ - if employee_id not in employee_leaves: + # Fetch employee data from the database + data = database.get_employee_data(employee_id) + if not data: return "Employee ID not found." - requested_days = len(leave_dates) - available_balance = employee_leaves[employee_id]["balance"] + if not leave_dates: + return "No leave dates provided. Please specify the dates you want to apply for." - if available_balance < requested_days: - return f"Insufficient leave balance. You requested {requested_days} day(s) but have only {available_balance}." + parsed_leave_dates = [] + today = datetime.date.today() + current_history = data.get('history', []) # Default to empty list if no history + + # Validate each date provided + for date_str in leave_dates: + # Check format "YYYY-MM-DD" + if not re.match(r"^\d{4}-\d{2}-\d{2}$", date_str): + return f"Invalid date format: '{date_str}'. Please use YYYY-MM-DD format." + # Check if the date is valid (e.g., not 2023-02-30) + try: + leave_date = datetime.datetime.strptime(date_str, "%Y-%m-%d").date() + except ValueError: + return f"Invalid date value: '{date_str}'. Please ensure dates are correct (e.g., not 2023-02-30)." + + # Check if the date is in the past + if leave_date < today: + return f"Cannot apply for leave in the past: '{date_str}' is before today ({today})." - # Deduct balance and add to history - employee_leaves[employee_id]["balance"] -= requested_days - employee_leaves[employee_id]["history"].extend(leave_dates) + # Check if the date is already booked + if date_str in current_history: + return f"Date {date_str} is already booked as leave for employee {employee_id}." - return f"Leave applied for {requested_days} day(s). Remaining balance: {employee_leaves[employee_id]['balance']}." + parsed_leave_dates.append(date_str) # Store validated dates (original string format) + + # Check for sufficient balance + requested_days = len(parsed_leave_dates) + available_balance = data["balance"] + + if available_balance < requested_days: + return f"Insufficient leave balance for {employee_id}. Requested: {requested_days} day(s), Available: {available_balance}." + + # Update database + new_balance = available_balance - requested_days + if database.update_employee_leave(employee_id, new_balance, parsed_leave_dates): + return f"Leave applied successfully for {requested_days} day(s) for {employee_id}. New balance: {new_balance}." + else: + # This case might indicate a concurrent modification or unexpected DB issue. + return "Failed to update leave records in the database. Please try again or contact support if the issue persists." -# Resource: Leave history @mcp.tool() def get_leave_history(employee_id: str) -> str: - """Get leave history for the employee""" - data = employee_leaves.get(employee_id) + """ + Retrieves the leave history for a given employee ID. + Returns a list of dates or a message if no leave has been taken. + Example: "Show me the leave history for E002" + """ + data = database.get_employee_data(employee_id) if data: - history = ', '.join(data['history']) if data['history'] else "No leaves taken." - return f"Leave history for {employee_id}: {history}" + history_list = data['history'] + if history_list: + # Format for better readability if there are many dates + history_str = ', '.join(sorted(list(set(history_list)))) # Sort and remove duplicates for display + return f"Leave history for employee {employee_id}: {history_str}." + else: + return f"Employee {employee_id} has no leave history." return "Employee ID not found." -# Resource: Greeting + +# --- Resource Definitions --- + @mcp.resource("greeting://{name}") def get_greeting(name: str) -> str: - """Get a personalized greeting""" + """ + Provides a personalized greeting. + This is a simple resource example. + Example URI: greeting://Alice + """ return f"Hello, {name}! How can I assist you with leave management today?" + +# --- Server Execution --- + if __name__ == "__main__": + # This allows running the MCP server directly using `python main.py` + # The database (leave_management.db) will be created/updated in the same directory. mcp.run() diff --git a/2_mcp_leave_management/pyproject.toml b/2_mcp_leave_management/pyproject.toml index 069f72d..a620229 100644 --- a/2_mcp_leave_management/pyproject.toml +++ b/2_mcp_leave_management/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "4-mcp-atliq" +name = "mcp_leave_management_tutorial" version = "0.1.0" -description = "Add your description here" +description = "A tutorial MCP server for managing employee leaves, demonstrating basic tool and resource creation." readme = "README.md" requires-python = ">=3.10" dependencies = [ diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..034e848 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc.