diff --git a/README.md b/README.md index 077d3d6c..6dd0d12a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -AgentStack Banner +# AgentStack [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/release/python-3100/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ![python-testing](https://github.com/agentops-ai/agentstack/actions/workflows/python-testing.yml/badge.svg) ![mypy](https://github.com/agentops-ai/agentstack/actions/workflows/mypy.yml/badge.svg) [![codecov.io](https://codecov.io/github/agentops-ai/agentstack/coverage.svg?branch=master)](https://codecov.io/github/agentops-ai/agentstack>?branch=master) @@ -12,10 +12,12 @@ AgentStack scaffolds your _agent stack_ - The tech stack that collectively is yo ### Install AgentStack ```sh -pip install agentstack -agentstack init +curl --proto '=https' --tlsv1.2 -LsSf https://install.agentstack.sh | sh ``` +or python [other install methods](https://docs.agentstack.sh/installation) + +### Start your agent! Create AI agent projects from the command line. @@ -53,7 +55,7 @@ Create a project, and you're good to go. To create a new agent project, run: ```sh -uv pip install agentstack +uv pip install agentstack # or other install method agentstack init ``` @@ -149,4 +151,4 @@ AgentStack is open source software [licensed as MIT](LICENSE). AgentStack is a new project built by passionate AI agent developers! We'd love help making this tool better. Easy first issues are available, create new issues with feature ideas, or chat with us on our [Discord](https://discord.gg/JdWkh9tgTQ). Make sure you read our contributor documentation to familiarize yourself with the project at [How to Contribute](https://docs.agentstack.sh/contributing/how-to-contribute). -If you are an Agent Tool developer, feel free to create an issue or even a PR to add your tool to AgentStack. \ No newline at end of file +If you are an Agent Tool developer, feel free to create an issue or even a PR to add your tool to AgentStack. diff --git a/agentstack/_tools/agentmail/__init__.py b/agentstack/_tools/agentmail/__init__.py new file mode 100644 index 00000000..142abc10 --- /dev/null +++ b/agentstack/_tools/agentmail/__init__.py @@ -0,0 +1,184 @@ +from agentmail import AgentMail +from typing import Optional, List + + +client = AgentMail() + + +def list_inboxes(limit: Optional[int] = None, last_key: Optional[str] = None): + """ + List inboxes. + + Args: + limit: The maximum number of inboxes to return. + last_key: The last key returned from the previous page. + + Returns: + A list of inboxes. + """ + return client.inboxes.list(limit=limit, last_key=last_key) + + +def get_inbox(inbox_id: str): + """ + Get an inbox by ID. + + Args: + inbox_id: The ID of the inbox to get. + + Returns: + An inbox. + """ + return client.inboxes.get(inbox_id) + + +def create_inbox( + username: Optional[str] = None, + domain: Optional[str] = None, + display_name: Optional[str] = None, +): + """ + Create an inbox. + + Args: + username: The username of the inbox. + domain: The domain of the inbox. + display_name: The display name of the inbox. + + Returns: + An inbox. + """ + return client.inboxes.create( + username=username, domain=domain, display_name=display_name + ) + + +def list_threads( + inbox_id: str, limit: Optional[int] = None, last_key: Optional[str] = None +): + """ + List threads in an inbox. + + Args: + inbox_id: The ID of the inbox to list threads for. + limit: The maximum number of threads to return. + last_key: The last key returned from the previous page. + + Returns: + A list of threads. + """ + return client.threads.list(inbox_id=inbox_id, limit=limit, last_key=last_key) + + +def get_thread(inbox_id: str, thread_id: str): + """ + Get a thread by ID. + + Args: + inbox_id: The ID of the inbox to get the thread for. + thread_id: The ID of the thread to get. + + Returns: + A thread. + """ + return client.threads.get(inbox_id=inbox_id, thread_id=thread_id) + + +def list_messages( + inbox_id: str, limit: Optional[int] = None, last_key: Optional[str] = None +): + """ + List messages in an inbox. + + Args: + inbox_id: The ID of the inbox to list messages for. + limit: The maximum number of messages to return. + last_key: The last key returned from the previous page. + + Returns: + A list of messages. + """ + return client.messages.list(inbox_id=inbox_id, limit=limit, last_key=last_key) + + +def get_message(inbox_id: str, message_id: str): + """ + Get a message by ID. + + Args: + inbox_id: The ID of the inbox to get the message for. + message_id: The ID of the message to get. + + Returns: + A message. + """ + return client.messages.get(inbox_id=inbox_id, message_id=message_id) + + +def get_attachment(inbox_id: str, message_id: str, attachment_id: str): + """ + Get an attachment by message ID and attachment ID. + + Args: + inbox_id: The ID of the inbox to get the attachment for. + message_id: The ID of the message to get the attachment for. + attachment_id: The ID of the attachment to get. + + Returns: + An attachment. + """ + return client.messages.get_attachment( + inbox_id=inbox_id, message_id=message_id, attachment_id=attachment_id + ) + + +def send_message( + inbox_id: str, + to: List[str], + cc: Optional[List[str]] = None, + bcc: Optional[List[str]] = None, + subject: Optional[str] = None, + text: Optional[str] = None, + html: Optional[str] = None, +): + """ + Send a message. + + Args: + inbox_id: The ID of the inbox to send the message from. + to: The list of recipients. + cc: The list of CC recipients. + bcc: The list of BCC recipients. + subject: The subject of the message. + text: The plain text body of the message. + html: The HTML body of the message. + + Returns: + A message. + """ + return client.messages.send( + inbox_id=inbox_id, to=to, cc=cc, bcc=bcc, subject=subject, text=text, html=html + ) + + +def reply_to_message( + inbox_id: str, + message_id: str, + text: Optional[str] = None, + html: Optional[str] = None, +): + """ + Reply to a message. + + Args: + inbox_id: The ID of the inbox to reply to the message in. + message_id: The ID of the message to reply to. + text: The plain text body of the reply. + html: The HTML body of the reply. + + Returns: + A message. + """ + return client.messages.reply( + inbox_id=inbox_id, message_id=message_id, text=text, html=html + ) diff --git a/agentstack/_tools/agentmail/config.json b/agentstack/_tools/agentmail/config.json new file mode 100644 index 00000000..71c981ba --- /dev/null +++ b/agentstack/_tools/agentmail/config.json @@ -0,0 +1,13 @@ +{ + "name": "agentmail", + "category": "email", + "tools": ["list_inboxes", "get_inbox", "create_inbox", "list_threads", "get_thread", "list_messages", "get_message", "get_attachment", "send_message", "reply_to_message"], + "url": "https://agentmail.to", + "cta": "Get your AgentMail API key at https://agentmail.to", + "env": { + "AGENTMAIL_API_KEY": null + }, + "dependencies": [ + "agentmail>=0.0.19" + ] +} \ No newline at end of file diff --git a/agentstack/_tools/agentql/__init__.py b/agentstack/_tools/agentql/__init__.py index 469fcfcf..7999958c 100644 --- a/agentstack/_tools/agentql/__init__.py +++ b/agentstack/_tools/agentql/__init__.py @@ -8,12 +8,18 @@ API_KEY = os.getenv("AGENTQL_API_KEY") -def query_data(url: str, query: Optional[str], prompt: Optional[str]) -> dict: + +def extract_data( + url: str, + query: Optional[str], + prompt: Optional[str], + is_stealth_mode_enabled: bool = False, +) -> dict: """ url: url of website to scrape query: described below prompt: Natural language description of the data you want to scrape - + is_stealth_mode_enabled: Enable stealth mode for web scraping (default: False) AgentQL query to scrape the url. @@ -47,12 +53,16 @@ def query_data(url: str, query: Optional[str], prompt: Optional[str]) -> dict: payload = { "url": url, "query": query, - "prompt": prompt + "prompt": prompt, + "metadata": { + "experimental_stealth_mode_enabled": is_stealth_mode_enabled, + }, } headers = { "X-API-Key": f"{API_KEY}", - "Content-Type": "application/json" + "Content-Type": "application/json", + "X-TF-Request-Origin": "agentstack", } try: @@ -60,21 +70,27 @@ def query_data(url: str, query: Optional[str], prompt: Optional[str]) -> dict: QUERY_DATA_ENDPOINT, headers=headers, json=payload, - timeout=API_TIMEOUT_SECONDS + timeout=API_TIMEOUT_SECONDS, ) response.raise_for_status() except httpx.HTTPStatusError as e: response = e.response if response.status_code in [401, 403]: - raise ValueError("Please, provide a valid API Key. You can create one at https://dev.agentql.com.") from e + raise ValueError( + "Please, provide a valid API Key. You can create one at https://dev.agentql.com." + ) from e else: try: error_json = response.json() - msg = error_json["error_info"] if "error_info" in error_json else error_json["detail"] + msg = ( + error_json["error_info"] + if "error_info" in error_json + else error_json["detail"] + ) except (ValueError, TypeError): msg = f"HTTP {e}." raise ValueError(msg) from e else: json = response.json() - return json["data"] \ No newline at end of file + return json["data"] diff --git a/agentstack/_tools/agentql/config.json b/agentstack/_tools/agentql/config.json index 154be031..b16b5ed3 100644 --- a/agentstack/_tools/agentql/config.json +++ b/agentstack/_tools/agentql/config.json @@ -6,6 +6,6 @@ "env": { "AGENTQL_API_KEY": "..." }, - "tools": ["query_data"], + "tools": ["extract_data"], "cta": "Create your AgentQL API key at https://dev.agentql.com" } diff --git a/agentstack/_tools/dappier/__init__.py b/agentstack/_tools/dappier/__init__.py new file mode 100644 index 00000000..84ea21b1 --- /dev/null +++ b/agentstack/_tools/dappier/__init__.py @@ -0,0 +1,255 @@ +import os +from typing import Optional, Literal +from dappier import Dappier + +# Initialize the Dappier client +client = Dappier(api_key=os.getenv("DAPPIER_API_KEY")) + +# --- Functions for AI Models --- + + +def real_time_web_search(query: str) -> str: + """ + Perform a real-time web search. Access the latest news, stock market data, weather, + travel information, deals, and more using this AI model. Use when no stock ticker symbol + is provided. + + Args: + query: The search query to retrieve real-time information. + + Returns: + A formatted string containing real-time search results. + """ + try: + return client.search_real_time_data_string(query=query, ai_model_id="am_01j06ytn18ejftedz6dyhz2b15") + except Exception as e: + return f"Error: {str(e)}" + + +def stock_market_data_search(query: str) -> str: + """ + Perform a real-time stock market data search. Retrieve real-time financial news, + stock prices, and trade updates with AI-powered insights using this model. Use only when a + stock ticker symbol is provided. + + Args: + query: The search query to retrieve real-time stock market information. + + Returns: + A formatted string containing real-time financial search results. + """ + try: + return client.search_real_time_data_string(query=query, ai_model_id="am_01j749h8pbf7ns8r1bq9s2evrh") + except Exception as e: + return f"Error: {str(e)}" + + +# --- Functions for Data Models --- + + +def get_sports_news( + query: str, + similarity_top_k: int = 9, + ref: Optional[str] = None, + num_articles_ref: int = 0, + search_algorithm: Literal["most_recent", "semantic", "most_recent_semantic", "trending"] = "most_recent", +) -> str: + """ + Fetch AI-powered Sports News recommendations. Get real-time news, updates, and personalized + content from top sports sources like Sportsnaut, Forever Blueshirts, Minnesota Sports Fan, + LAFB Network, Bounding Into Sports, and Ringside Intel. + + Args: + query: The input string for sports-related content recommendations. + similarity_top_k: Number of top similar articles to retrieve. + ref: Optional site domain to prioritize recommendations. + num_articles_ref: Minimum number of articles to return from the reference domain. + search_algorithm: The search algorithm to use ('most_recent', 'semantic', 'most_recent_semantic', 'trending'). + + Returns: + A formatted string containing recommended sports articles. + """ + try: + return client.get_ai_recommendations_string( + query=query, + data_model_id="dm_01j0pb465keqmatq9k83dthx34", + similarity_top_k=similarity_top_k, + ref=ref or "", + num_articles_ref=num_articles_ref, + search_algorithm=search_algorithm, + ) + except Exception as e: + return f"Error: {str(e)}" + + +def get_lifestyle_news( + query: str, + similarity_top_k: int = 9, + ref: Optional[str] = None, + num_articles_ref: int = 0, + search_algorithm: Literal["most_recent", "semantic", "most_recent_semantic", "trending"] = "most_recent", +) -> str: + """ + Fetch AI-powered Lifestyle News recommendations. Access current lifestyle updates, analysis, + and insights from leading lifestyle publications like The Mix, Snipdaily, Nerdable + and Familyproof. + + Args: + query: The input string for lifestyle-related content recommendations. + similarity_top_k: Number of top similar articles to retrieve. + ref: Optional site domain to prioritize recommendations. + num_articles_ref: Minimum number of articles to return from the reference domain. + search_algorithm: The search algorithm to use ('most_recent', 'semantic', 'most_recent_semantic', 'trending'). + + Returns: + A formatted string containing recommended lifestyle articles. + """ + try: + return client.get_ai_recommendations_string( + query=query, + data_model_id="dm_01j0q82s4bfjmsqkhs3ywm3x6y", + similarity_top_k=similarity_top_k, + ref=ref or "", + num_articles_ref=num_articles_ref, + search_algorithm=search_algorithm, + ) + except Exception as e: + return f"Error: {str(e)}" + + +def get_iheartdogs_content( + query: str, + similarity_top_k: int = 9, + ref: Optional[str] = None, + num_articles_ref: int = 0, + search_algorithm: Literal["most_recent", "semantic", "most_recent_semantic", "trending"] = "most_recent", +) -> str: + """ + Fetch AI-powered iHeartDogs content recommendations. Tap into a dog care expert with access + to thousands of articles covering pet health, behavior, grooming, and ownership from + iHeartDogs.com. + + Args: + query: The input string for dog care-related content recommendations. + similarity_top_k: Number of top similar articles to retrieve. + ref: Optional site domain to prioritize recommendations. + num_articles_ref: Minimum number of articles to return from the reference domain. + search_algorithm: The search algorithm to use ('most_recent', 'semantic', 'most_recent_semantic', 'trending'). + + Returns: + A formatted string containing recommended dog-related articles. + """ + try: + return client.get_ai_recommendations_string( + query=query, + data_model_id="dm_01j1sz8t3qe6v9g8ad102kvmqn", + similarity_top_k=similarity_top_k, + ref=ref or "", + num_articles_ref=num_articles_ref, + search_algorithm=search_algorithm, + ) + except Exception as e: + return f"Error: {str(e)}" + + +def get_iheartcats_content( + query: str, + similarity_top_k: int = 9, + ref: Optional[str] = None, + num_articles_ref: int = 0, + search_algorithm: Literal["most_recent", "semantic", "most_recent_semantic", "trending"] = "most_recent", +) -> str: + """ + Fetch AI-powered iHeartCats content recommendations. Utilize a cat care specialist that + provides comprehensive content on cat health, behavior, and lifestyle from iHeartCats.com. + + Args: + query: The input string for cat care-related content recommendations. + similarity_top_k: Number of top similar articles to retrieve. + ref: Optional site domain to prioritize recommendations. + num_articles_ref: Minimum number of articles to return from the reference domain. + search_algorithm: The search algorithm to use ('most_recent', 'semantic', 'most_recent_semantic', 'trending'). + + Returns: + A formatted string containing recommended cat-related articles. + """ + try: + return client.get_ai_recommendations_string( + query=query, + data_model_id="dm_01j1sza0h7ekhaecys2p3y0vmj", + similarity_top_k=similarity_top_k, + ref=ref or "", + num_articles_ref=num_articles_ref, + search_algorithm=search_algorithm, + ) + except Exception as e: + return f"Error: {str(e)}" + + +def get_greenmonster_guides( + query: str, + similarity_top_k: int = 9, + ref: Optional[str] = None, + num_articles_ref: int = 0, + search_algorithm: Literal["most_recent", "semantic", "most_recent_semantic", "trending"] = "most_recent", +) -> str: + """ + Fetch AI-powered GreenMonster guides and articles. Receive guidance for making conscious + and compassionate choices benefiting people, animals, and the planet. + + Args: + query: The input string for eco-friendly and conscious lifestyle recommendations. + similarity_top_k: Number of top similar articles to retrieve. + ref: Optional site domain to prioritize recommendations. + num_articles_ref: Minimum number of articles to return from the reference domain. + search_algorithm: The search algorithm to use ('most_recent', 'semantic', 'most_recent_semantic', 'trending'). + + Returns: + A formatted string containing recommended eco-conscious articles. + """ + try: + return client.get_ai_recommendations_string( + query=query, + data_model_id="dm_01j5xy9w5sf49bm6b1prm80m27", + similarity_top_k=similarity_top_k, + ref=ref or "", + num_articles_ref=num_articles_ref, + search_algorithm=search_algorithm, + ) + except Exception as e: + return f"Error: {str(e)}" + + +def get_wishtv_news( + query: str, + similarity_top_k: int = 9, + ref: Optional[str] = None, + num_articles_ref: int = 0, + search_algorithm: Literal["most_recent", "semantic", "most_recent_semantic", "trending"] = "most_recent", +) -> str: + """ + Fetch AI-powered WISH-TV news recommendations. Get recommendations covering sports, + breaking news, politics, multicultural updates, Hispanic language content, entertainment, + health, and education. + + Args: + query: The input string for general news recommendations. + similarity_top_k: Number of top similar articles to retrieve. + ref: Optional site domain to prioritize recommendations. + num_articles_ref: Minimum number of articles to return from the reference domain. + search_algorithm: The search algorithm to use ('most_recent', 'semantic', 'most_recent_semantic', 'trending'). + + Returns: + A formatted string containing recommended news articles. + """ + try: + return client.get_ai_recommendations_string( + query=query, + data_model_id="dm_01jagy9nqaeer9hxx8z1sk1jx6", + similarity_top_k=similarity_top_k, + ref=ref or "", + num_articles_ref=num_articles_ref, + search_algorithm=search_algorithm, + ) + except Exception as e: + return f"Error: {str(e)}" diff --git a/agentstack/_tools/dappier/config.json b/agentstack/_tools/dappier/config.json new file mode 100644 index 00000000..715a41a4 --- /dev/null +++ b/agentstack/_tools/dappier/config.json @@ -0,0 +1,20 @@ +{ + "name": "dappier", + "url": "https://www.dappier.com/", + "category": "search", + "env": { + "DAPPIER_API_KEY": null + }, + "dependencies": ["dappier>=0.3.5"], + "tools": [ + "real_time_web_search", + "stock_market_data_search", + "get_sports_news", + "get_lifestyle_news", + "get_iheartdogs_content", + "get_iheartcats_content", + "get_greenmonster_guides", + "get_wishtv_news" + ], + "cta": "Create an API key at https://platform.dappier.com/profile/api-keys/" +} diff --git a/agentstack/_tools/hyperbrowser/__init__.py b/agentstack/_tools/hyperbrowser/__init__.py new file mode 100644 index 00000000..2760ef8d --- /dev/null +++ b/agentstack/_tools/hyperbrowser/__init__.py @@ -0,0 +1,261 @@ +import os +from typing import List + +from hyperbrowser import Hyperbrowser +from hyperbrowser.models import ( + BrowserUseTaskResponse, + ClaudeComputerUseTaskResponse, + CrawlJobResponse, + CreateSessionParams, + CuaTaskResponse, + ExtractJobResponse, + ScrapeFormat, + ScrapeJobResponse, + ScrapeOptions, + StartBrowserUseTaskParams, + StartClaudeComputerUseTaskParams, + StartCrawlJobParams, + StartCuaTaskParams, + StartExtractJobParams, + StartScrapeJobParams, +) + +hb = Hyperbrowser(api_key=os.getenv('HYPERBROWSER_API_KEY')) + + +def scrape_webpage( + url: str, use_proxy: bool = True, formats: list[ScrapeFormat] = ["markdown"] +) -> ScrapeJobResponse: + """ + Scrapes content from a single webpage in specified formats. + + This function initiates a scraping job for a given URL and waits for completion. + It configures a browser session with proxy and stealth options for optimal scraping. + + Args: + url: The URL of the webpage to scrape + use_proxy: Whether to use a proxy for the request (default: True) + formats: List of formats to return the scraped content in (default: ["markdown"]) + Options include "markdown", "html", "links", "screenshot" + + Returns: + ScrapeJobResponse: The response containing the scraped content in requested formats + """ + return hb.scrape.start_and_wait( + StartScrapeJobParams( + url=url, + session_options=CreateSessionParams( + use_proxy=use_proxy, + use_stealth=True, + adblock=True, + trackers=True, + annoyances=True, + ), + scrape_options=ScrapeOptions( + formats=formats, + ), + ) + ) + + +def crawl_website( + starting_url: str, + max_pages: int = 10, + include_pattern: List[str] = [], + exclude_pattern: List[str] = [], + use_proxy: bool = True, +) -> CrawlJobResponse: + """ + Crawls a website starting from a specific URL and collects content from multiple pages. + + This function navigates through a website by following links from the starting URL, + up to the specified maximum number of pages. It can filter pages to crawl based on + include and exclude patterns. + + Args: + starting_url: The initial URL to start crawling from + max_pages: Maximum number of pages to crawl (default: 10) + include_pattern: List of patterns for URLs to include in the crawl (default: []) + exclude_pattern: List of patterns for URLs to exclude from the crawl (default: []) + use_proxy: Whether to use a proxy for the requests (default: True) + + Returns: + CrawlJobResponse: The response containing the crawled content from all visited pages + """ + return hb.crawl.start_and_wait( + StartCrawlJobParams( + url=starting_url, + max_pages=max_pages, + include_pattern=include_pattern, + exclude_pattern=exclude_pattern, + session_options=CreateSessionParams( + use_proxy=use_proxy, + use_stealth=True, + adblock=True, + trackers=True, + annoyances=True, + ), + ) + ) + + +def extract_data_from_webpages( + urls: List[str], + schema: str, + prompt: str, + system_prompt: str | None = None, + use_proxy: bool = True, +) -> ExtractJobResponse: + """ + Extracts structured data from multiple webpages based on a provided schema and prompt. + + This function visits each URL in the list and extracts structured data according to the + specified schema and guided by the provided prompt. It uses AI-powered extraction to + transform unstructured web content into structured data. + + Args: + urls: List of URLs to extract data from + schema: JSON schema that defines the structure of the data to extract + prompt: Instructions for the extraction model on what data to extract + system_prompt: Optional system prompt to further guide the extraction (default: None) + use_proxy: Whether to use a proxy for the requests (default: True) + + Returns: + ExtractJobResponse: The response containing the extracted structured data from all URLs + """ + return hb.extract.start_and_wait( + StartExtractJobParams( + urls=urls, + prompt=prompt, + system_prompt=system_prompt, + schema=schema, + session_options=CreateSessionParams( + use_proxy=use_proxy, + use_stealth=True, + adblock=True, + ), + ) + ) + + +def run_browser_use_agent( + task: str, + max_steps: int = 10, + use_vision: bool = False, + use_vision_for_planner: bool = False, + use_proxy: bool = True, +) -> BrowserUseTaskResponse: + """ + Runs a lightweight browser automation agent to perform a specific task. + + This function initiates a browser session and runs a specialized agent that + performs the specified task with minimal overhead. This agent is optimized for + speed and efficiency but requires explicit, detailed instructions. + + Args: + task: Detailed description of the task to perform + max_steps: Maximum number of steps the agent can take (default: 10) + use_vision: Whether to enable vision capabilities for the agent (default: False) + use_vision_for_planner: Whether to use vision for planning steps (default: False) + use_proxy: Whether to use a proxy for the browser session (default: True) + + Returns: + BrowserUseTaskResponse: The response containing the results of the task execution + """ + return hb.agents.browser_use.start_and_wait( + StartBrowserUseTaskParams( + task=task, + max_steps=max_steps, + use_vision=use_vision, + use_vision_for_planner=use_vision_for_planner, + session_options=CreateSessionParams( + use_proxy=use_proxy, + use_stealth=True, + adblock=True, + trackers=True, + annoyances=True, + ), + ) + ) + + +def run_claude_computer_use_agent( + task: str, + max_steps: int = 10, + use_vision: bool = False, + use_vision_for_planner: bool = False, + use_proxy: bool = True, +) -> ClaudeComputerUseTaskResponse: + """ + Runs a Claude-powered browser automation agent to perform complex tasks. + + This function initiates a browser session with Anthropic's Claude model as the + driving intelligence. The agent is capable of sophisticated reasoning and handling + complex, nuanced tasks that require understanding context and making decisions. + + Args: + task: Description of the task to perform + max_steps: Maximum number of steps the agent can take (default: 10) + use_vision: Whether to enable vision capabilities for the agent (default: False) + use_vision_for_planner: Whether to use vision for planning steps (default: False) + use_proxy: Whether to use a proxy for the browser session (default: True) + + Returns: + ClaudeComputerUseTaskResponse: The response containing the results of the task execution + """ + return hb.agents.claude_computer_use.start_and_wait( + StartClaudeComputerUseTaskParams( + task=task, + max_steps=max_steps, + use_vision=use_vision, + use_vision_for_planner=use_vision_for_planner, + session_options=CreateSessionParams( + use_proxy=use_proxy, + use_stealth=True, + adblock=True, + trackers=True, + annoyances=True, + ), + ) + ) + + +def run_openai_cua_agent( + task: str, + max_steps: int = 10, + use_vision: bool = False, + use_vision_for_planner: bool = False, + use_proxy: bool = True, +) -> CuaTaskResponse: + """ + Runs an OpenAI-powered browser automation agent to perform general-purpose tasks. + + This function initiates a browser session with OpenAI's model as the driving + intelligence. The agent provides balanced performance and reliability for a wide range + of browser automation tasks with moderate complexity. + + Args: + task: Description of the task to perform + max_steps: Maximum number of steps the agent can take (default: 10) + use_vision: Whether to enable vision capabilities for the agent (default: False) + use_vision_for_planner: Whether to use vision for planning steps (default: False) + use_proxy: Whether to use a proxy for the browser session (default: True) + + Returns: + CuaTaskResponse: The response containing the results of the task execution + """ + return hb.agents.cua.start_and_wait( + StartCuaTaskParams( + task=task, + max_steps=max_steps, + use_vision=use_vision, + use_vision_for_planner=use_vision_for_planner, + session_options=CreateSessionParams( + use_proxy=use_proxy, + use_stealth=True, + adblock=True, + trackers=True, + annoyances=True, + ), + ) + ) diff --git a/agentstack/_tools/hyperbrowser/config.json b/agentstack/_tools/hyperbrowser/config.json new file mode 100644 index 00000000..646c063d --- /dev/null +++ b/agentstack/_tools/hyperbrowser/config.json @@ -0,0 +1,18 @@ +{ + "name": "hyperbrowser", + "url": "https://hyperbrowser.ai/", + "category": "browsing", + "env": { + "HYPERBROWSER_API_KEY": null + }, + "dependencies": ["hyperbrowser>=0.39.0"], + "tools": [ + "scrape_webpage", + "crawl_website", + "extract_data_from_webpages", + "run_browser_use_agent", + "run_claude_computer_use_agent", + "run_openai_cua_agent" + ], + "cta": "Get your free API key at https://hyperbrowser.ai/" +} diff --git a/agentstack/_tools/hyperspell/__init__.py b/agentstack/_tools/hyperspell/__init__.py new file mode 100644 index 00000000..d7d4d952 --- /dev/null +++ b/agentstack/_tools/hyperspell/__init__.py @@ -0,0 +1,138 @@ +import os +import json +from typing import List, Optional, Dict, Any +from pathlib import Path +from hyperspell import Hyperspell + +# Get environment variables +HYPERSPELL_API_KEY = os.getenv('HYPERSPELL_API_KEY') +default_user_id = os.getenv('HYPERSPELL_USER_ID') + + +def hyperspell_search(query: str, sources: Optional[str] = None, answer: bool = False, user_id: Optional[str] = None) -> str: + """ + Search across your HyperSpell knowledge base (documents, integrations like Notion, Gmail, etc.). + + Args: + query: The search query to find relevant information + sources: Comma-separated list of sources to search (e.g., "collections,notion,gmail"). + If None, searches all available sources. + answer: If True, returns a direct answer to the query instead of just documents + user_id: Optional user ID to use for this request. Defaults to HYPERSPELL_USER_ID env var. + + Returns: + JSON string containing search results or answer + """ + try: + # Create client for this request + client = Hyperspell(api_key=HYPERSPELL_API_KEY, user_id=user_id or default_user_id) + # Parse sources if provided + sources_list = sources.split(',') if sources else None + + # Build options based on sources + options = {} + if sources_list and 'collections' in sources_list: + options['collections'] = {} + + response = client.query.search( + query=query, + sources=sources_list or ["collections"], + answer=answer, + options=options + ) + + if answer: + return json.dumps({ + "answer": response.answer, + "sources_used": [doc.source for doc in response.documents], + "document_count": len(response.documents) + }) + else: + return json.dumps({ + "documents": [ + { + "title": getattr(doc, 'filename', getattr(doc, 'title', 'No title')), + "content": getattr(doc, 'summary', 'No content available')[:500] + "..." if len(getattr(doc, 'summary', '')) > 500 else getattr(doc, 'summary', 'No content available'), + "source": doc.source, + "score": getattr(doc, 'score', 0), + "resource_id": doc.resource_id, + "content_type": getattr(doc, 'content_type', None) + } + for doc in response.documents + ], + "total_results": len(response.documents) + }) + + except Exception as e: + return json.dumps({"error": f"Error searching HyperSpell: {str(e)}"}) + + +def hyperspell_add_document(text: str, title: Optional[str] = None, collection: Optional[str] = None, user_id: Optional[str] = None) -> str: + """ + Add a text document to your HyperSpell knowledge base. + + Args: + text: The full text content to add + title: Optional title for the document + collection: Optional collection name to organize the document + user_id: Optional user ID to use for this request. Defaults to HYPERSPELL_USER_ID env var. + + Returns: + JSON string with the document ID and status + """ + try: + # Create client for this request + client = Hyperspell(api_key=HYPERSPELL_API_KEY, user_id=user_id or default_user_id) + response = client.documents.add( + text=text, + title=title, + collection=collection + ) + + return json.dumps({ + "document_id": response.id, + "resource_id": response.resource_id, + "status": response.status, + "collection": collection + }) + + except Exception as e: + return json.dumps({"error": f"Error adding document to HyperSpell: {str(e)}"}) + + +def hyperspell_upload_file(file_path: str, collection: Optional[str] = None, user_id: Optional[str] = None) -> str: + """ + Upload a file (PDF, Word doc, spreadsheet, etc.) to your HyperSpell knowledge base. + + Args: + file_path: Path to the file to upload + collection: Optional collection name to organize the document + user_id: Optional user ID to use for this request. Defaults to HYPERSPELL_USER_ID env var. + + Returns: + JSON string with the document ID and status + """ + try: + # Create client for this request + client = Hyperspell(api_key=HYPERSPELL_API_KEY, user_id=user_id or default_user_id) + # Convert to Path object for proper MIME type detection + file_path_obj = Path(file_path) + + if not file_path_obj.exists(): + return json.dumps({"error": f"File not found: {file_path}"}) + + response = client.documents.upload( + file=file_path_obj, # Use Path object directly + collection=collection + ) + + return json.dumps({ + "document_id": response.id, + "resource_id": response.resource_id, + "status": response.status, + "filename": file_path_obj.name, + "collection": collection + }) + + except Exception as e: + return json.dumps({"error": f"Error uploading file to HyperSpell: {str(e)}"}) \ No newline at end of file diff --git a/agentstack/_tools/hyperspell/config.json b/agentstack/_tools/hyperspell/config.json new file mode 100644 index 00000000..c70151a9 --- /dev/null +++ b/agentstack/_tools/hyperspell/config.json @@ -0,0 +1,14 @@ +{ + "name": "hyperspell", + "url": "https://hyperspell.com", + "category": "knowledge", + "env": { + "HYPERSPELL_API_KEY": null, + "HYPERSPELL_USER_ID": null + }, + "dependencies": [ + "hyperspell>=0.1.0" + ], + "tools": ["hyperspell_search", "hyperspell_add_document", "hyperspell_upload_file"], + "cta": "Get your HyperSpell API key at https://app.hyperspell.com/dashboard and set your user ID" + } \ No newline at end of file diff --git a/agentstack/conf.py b/agentstack/conf.py index 64319f4b..fe0e6432 100644 --- a/agentstack/conf.py +++ b/agentstack/conf.py @@ -15,12 +15,16 @@ PATH: Path = Path() +class NoProjectError(Exception): + pass + + def assert_project() -> None: try: ConfigFile() return except FileNotFoundError: - raise Exception("Could not find agentstack.json, are you in an AgentStack project directory?") + raise NoProjectError("Could not find agentstack.json, are you in an AgentStack project directory?") def set_path(path: Union[str, Path, None]): diff --git a/agentstack/packaging.py b/agentstack/packaging.py index e226db96..73d5fb98 100644 --- a/agentstack/packaging.py +++ b/agentstack/packaging.py @@ -4,6 +4,7 @@ import re import subprocess import select +import site from packaging.requirements import Requirement from agentstack import conf, log @@ -20,10 +21,17 @@ # In testing, when this was not set, packages could end up in the pyenv's # site-packages directory; it's possible an environment variable can control this. +_python_executable = ".venv/bin/python" + +def set_python_executable(path: str): + global _python_executable + + _python_executable = path + def install(package: str): """Install a package with `uv` and add it to pyproject.toml.""" - + global _python_executable from agentstack.cli.spinner import Spinner def on_progress(line: str): @@ -35,7 +43,7 @@ def on_error(line: str): with Spinner(f"Installing {package}") as spinner: _wrap_command_with_callbacks( - [get_uv_bin(), 'add', '--python', '.venv/bin/python', package], + [get_uv_bin(), 'add', '--python', _python_executable, package], on_progress=on_progress, on_error=on_error, ) @@ -43,7 +51,7 @@ def on_error(line: str): def install_project(): """Install all dependencies for the user's project.""" - + global _python_executable from agentstack.cli.spinner import Spinner def on_progress(line: str): @@ -56,14 +64,14 @@ def on_error(line: str): try: with Spinner(f"Installing project dependencies.") as spinner: result = _wrap_command_with_callbacks( - [get_uv_bin(), 'pip', 'install', '--python', '.venv/bin/python', '.'], + [get_uv_bin(), 'pip', 'install', '--python', _python_executable, '.'], on_progress=on_progress, on_error=on_error, ) if result is False: spinner.clear_and_log("Retrying uv installation with --no-cache flag...", 'info') _wrap_command_with_callbacks( - [get_uv_bin(), 'pip', 'install', '--no-cache', '--python', '.venv/bin/python', '.'], + [get_uv_bin(), 'pip', 'install', '--no-cache', '--python', _python_executable, '.'], on_progress=on_progress, on_error=on_error, ) @@ -87,13 +95,13 @@ def on_error(line: str): log.info(f"Uninstalling {requirement.name}") _wrap_command_with_callbacks( - [get_uv_bin(), 'remove', '--python', '.venv/bin/python', requirement.name], + [get_uv_bin(), 'remove', '--python', _python_executable, requirement.name], on_progress=on_progress, on_error=on_error, ) -def upgrade(package: str): +def upgrade(package: str, use_venv: bool = True): """Upgrade a package with `uv`.""" # TODO should we try to update the project's pyproject.toml as well? @@ -104,11 +112,17 @@ def on_progress(line: str): def on_error(line: str): log.error(f"uv: [error]\n {line.strip()}") + extra_args = [] + if not use_venv: + # uv won't let us install without a venv if we don't specify a target + extra_args = ['--target', site.getusersitepackages()] + log.info(f"Upgrading {package}") _wrap_command_with_callbacks( - [get_uv_bin(), 'pip', 'install', '-U', '--python', '.venv/bin/python', package], + [get_uv_bin(), 'pip', 'install', '-U', '--python', _python_executable, *extra_args, package], on_progress=on_progress, on_error=on_error, + use_venv=use_venv, ) @@ -156,19 +170,21 @@ def _wrap_command_with_callbacks( on_progress: Callable[[str], None] = lambda x: None, on_complete: Callable[[str], None] = lambda x: None, on_error: Callable[[str], None] = lambda x: None, + use_venv: bool = True, ) -> bool: """Run a command with progress callbacks. Returns bool for cmd success.""" process = None try: all_lines = '' - process = subprocess.Popen( - command, - cwd=conf.PATH.absolute(), - env=_setup_env(), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) + sub_args = { + 'cwd': conf.PATH.absolute(), + 'stdout': subprocess.PIPE, + 'stderr': subprocess.PIPE, + 'text': True, + } + if use_venv: + sub_args['env'] = _setup_env() + process = subprocess.Popen(command, **sub_args) # type: ignore assert process.stdout and process.stderr # appease type checker readable = [process.stdout, process.stderr] diff --git a/agentstack/repo.py b/agentstack/repo.py index f1e10465..0ead48b0 100644 --- a/agentstack/repo.py +++ b/agentstack/repo.py @@ -1,11 +1,10 @@ from typing import Optional +from types import ModuleType from pathlib import Path import shutil -import git from agentstack import conf, log from agentstack.exceptions import EnvironmentError - MAIN_BRANCH_NAME = "main" AUTOMATION_NOTE = "\n\n(This commit was made automatically by AgentStack)" @@ -15,6 +14,14 @@ _USE_GIT = None # global state to disable git for this run +# The python git module prints an excessive error message when git is not +# installed. We always want to allow git support to fail silently. +try: + import git +except ImportError: + _USE_GIT = False + + def should_track_changes() -> bool: """ If git has been disabled for this run, return False. Next, look for the value diff --git a/agentstack/update.py b/agentstack/update.py index 2787e3d4..c1f9eae8 100644 --- a/agentstack/update.py +++ b/agentstack/update.py @@ -4,7 +4,7 @@ from pathlib import Path from packaging.version import parse as parse_version, Version import inquirer -from agentstack import log +from agentstack import conf, log from agentstack.utils import term_color, get_version, get_framework, get_base_dir from agentstack import packaging @@ -24,7 +24,7 @@ USER_GUID_FILE_PATH = get_base_dir() / ".cli-user-guid" INSTALL_PATH = Path(sys.executable).parent.parent ENDPOINT_URL = "https://pypi.org/simple" -CHECK_EVERY = 3600 # hour +CHECK_EVERY = 12 * 60 * 60 # 12 hours def _is_ci_environment(): @@ -113,7 +113,15 @@ def check_for_updates(update_requested: bool = False): if inquirer.confirm( f"New version of {AGENTSTACK_PACKAGE} available: {latest_version}! Do you want to install?" ): - packaging.upgrade(f'{AGENTSTACK_PACKAGE}[{get_framework()}]') + try: + # handle update inside a user project + conf.assert_project() + packaging.upgrade(f'{AGENTSTACK_PACKAGE}[{get_framework()}]') + except conf.NoProjectError: + # handle update for system version of agentstack + packaging.set_python_executable(sys.executable) + packaging.upgrade(AGENTSTACK_PACKAGE, use_venv=False) + log.success(f"{AGENTSTACK_PACKAGE} updated. Re-run your command to use the latest version.") else: log.info("Skipping update. Run `agentstack update` to install the latest version.") diff --git a/docs/images/the_agent_stack.png b/docs/images/the_agent_stack.png index d51fe064..802d82c9 100644 Binary files a/docs/images/the_agent_stack.png and b/docs/images/the_agent_stack.png differ diff --git a/docs/installation.mdx b/docs/installation.mdx index 042db2eb..e46b7ef9 100644 --- a/docs/installation.mdx +++ b/docs/installation.mdx @@ -4,6 +4,12 @@ description: 'Installing AgentStack is super easy!' icon: 'cloud-arrow-down' --- +## Using our Installer +_We recommend using this method_ +```bash +curl --proto '=https' --tlsv1.2 -LsSf https://install.agentstack.sh | sh +``` + ## Installing with Brew ```bash brew tap agentops-ai/tap diff --git a/docs/llms.txt b/docs/llms.txt index e904cfa5..d273a7cf 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -1287,6 +1287,7 @@ description: 'AgentStack tools from community contributors' ## Search - [Perplexity](/tools/tool/perplexity) +- [Dappier](/tools/tool/dappier) ## Memory / State diff --git a/docs/tools/community.mdx b/docs/tools/community.mdx index ae703bb4..e70849e2 100644 --- a/docs/tools/community.mdx +++ b/docs/tools/community.mdx @@ -3,6 +3,9 @@ title: 'Community Tools' description: 'AgentStack tools from community contributors' --- +## Email + - [AgentMail](/tools/tool/agentmail) + ## Web Retrieval - [AgentQL](/tools/tool/agentql) @@ -10,13 +13,17 @@ description: 'AgentStack tools from community contributors' [//]: # (- [Browserbase](/tools/tool/browserbase)) - [Firecrawl](/tools/tool/firecrawl) +- [Hyperbrowser](/tools/tool/hyperbrowser) ## Search - [Perplexity](/tools/tool/perplexity) +- [Dappier](/tools/tool/dappier) + ## Memory / State - [Mem0](/tools/tool/mem0) +- [Hyperspell](/tools/tool/hyperspell) ## Database Tools - [Neon](/tools/tool/neon) @@ -43,4 +50,4 @@ description: 'AgentStack tools from community contributors' > Default tools in AgentStack - \ No newline at end of file + diff --git a/docs/tools/tool/agentmail.mdx b/docs/tools/tool/agentmail.mdx new file mode 100644 index 00000000..fa5f3a9d --- /dev/null +++ b/docs/tools/tool/agentmail.mdx @@ -0,0 +1,42 @@ +--- +title: AgentMail +description: Email for agents +icon: email +--- + +[AgentMail](https://agentmail.to) is an API-first email provider designed to give agents their own inboxes for sending, receiving, and managing email. + +## Tools + +- List inboxes +- Get inbox +- Create inbox +- List threads +- Get thread +- List messages +- Get message +- Get attachment +- Send message +- Reply to message + +## Installation + +Add the AgentMail tool to your project + +```sh +agentstack tools add agentmail +``` + +Get your [AgentMail API key](https://agentmail.to) and set the environment variable + +```env +AGENTMAIL_API_KEY=your-agentmail-api-key +``` + +## Usage + +Use the AgentMail API to create an inbox for your agent. Then prompt your agent to handle any email related tasks. + +## Examples + +Coming soon... diff --git a/docs/tools/tool/agentql.mdx b/docs/tools/tool/agentql.mdx index f023dc05..9cd70f33 100644 --- a/docs/tools/tool/agentql.mdx +++ b/docs/tools/tool/agentql.mdx @@ -4,14 +4,12 @@ description: Precise web data extraction for agents icon: browser --- -AgentQL Web Loader is a tool that Scrape a url with a given AgentQL query or a natural language description of the data you want to scrape. +AgentQL Web Loader is a tool that scrapes a URL with a given AgentQL query or a Natural Language description of the data you want to scrape. Create your own AgentQL API key [here](https://dev.agentql.com). ## Description -AgentQL Web Loader is powered by AgentQL, an AI-powered query language for scraping web sites and automating workflows. -If you want to extract data in a precise format, use AgentQL natural language queries under the `query` field to pinpoint data on any web page, including authenticated and dynamically generated content. -Users can define structured data output and apply transforms within queries. -You could also directly describe the data you want to extract under the `prompt` field to let AgentQL Web Loader automatically generate the format and data. + +[AgentQL](https://agentql.com) provides structured data extraction from any web page using an [AgentQL query](https://docs.agentql.com/concepts/query-language) or a Natural Language prompt. AgentQL can be used across multiple languages and web pages without breaking over time and change. ## Installation @@ -19,7 +17,7 @@ You could also directly describe the data you want to extract under the `prompt` agentstack tools add agentql ``` -Set the environment variable +Set the environment variable in your project's `.env` file. ```env AGENTQL_API_KEY=... @@ -29,10 +27,15 @@ AGENTQL_API_KEY=... The following parameters can be used to customize the `AgentQL Web Loader`'s behavior: -| Argument | Type | Description | -|:---------------|:---------|:-------------------------------------------------------------------------------------------------------------------------------------| -| **url** | `string` | Url of the website to scrape from. | -| **query** | `string` | _Optional_. AgentQL query to scrape the url. Please visit [AgentQL Query Language Introduction](https://docs.agentql.com/agentql-query) for more information. | -| **prompt** | `string` | _Optional_. Natural language description of the data you want to scrape. Either `query` or `prompt` is required. | +| Argument | Type | Description | +| :-------------------------- | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **url** | `string` | The URL of the web page you want to query. | +| **query** | `string` | _Optional_. The AgentQL query to execute. Learn more about [how to write an AgentQL query in the docs](https://docs.agentql.com/agentql-query). | +| **prompt** | `string` | _Optional_. A Natural Language description of the data to query the page for. AgentQL will infer the data’s structure from your prompt. **Note: You must define either a `query` or a `prompt` to use AgentQL.** | +| **is_stealth_mode_enabled** | `boolean` | Whether to enable experimental anti-bot evasion strategies. This feature may not work for all websites at all times. Data extraction may take longer to complete with this mode enabled. **Defaults to `False`.** | + +## Examples -If you want to hack our tool, feel free to do so by modifying `src/tools/agentql_tool.py` and reference our documentation for [AgentQL REST API](https://docs.agentql.com/rest-api/api-reference). \ No newline at end of file +- [Research Assistant](https://github.com/AgentOps-AI/AgentStack/tree/main/examples/research_assistant) +- [Sentiment Analyzer](https://github.com/AgentOps-AI/AgentStack/tree/main/examples/sentiment_analyser) +- [Market Monitoring](https://github.com/AgentOps-AI/AgentStack/tree/main/examples/market_monitoring) diff --git a/docs/tools/tool/dappier.mdx b/docs/tools/tool/dappier.mdx new file mode 100644 index 00000000..816dd728 --- /dev/null +++ b/docs/tools/tool/dappier.mdx @@ -0,0 +1,66 @@ +--- +title: Dappier +description: Real-time web and content search for agents +icon: search +--- + +Dappier is a real time search that connects any AI to proprietary, real-time data — including web search, news, sports, stock market data, and premium publisher content. + +## Description +Dappier Real-Time Search provides instant access to live web search results and AI-powered recommendations with: + +- Real-Time Web Search offering up-to-the-minute results from Google, financial markets, and global news +- Specialized Content Models trained on curated datasets for domains like sports, lifestyle, pet care, sustainability, and multicultural news +- Intelligent Query Routing that automatically selects the appropriate model based on user input + +### Core Features: + +- Web Search - Perform real-time web lookups across news, stocks, travel, weather, and more +- Stock Market Data - Retrieve live financial news, stock prices, and trades +- Content Recommendations - Get semantically matched articles tailored to user interests +- Domain-Specific Models - Specialized AI trained on lifestyle, pets, sports, and green living + +### Output Formats: + +- Summarized real-time search results +- Curated lists of recommended articles +- Live financial and stock market insights +- Structured query-to-content responses + +## Available Models and Functions + +> Explore various AI models and data models available at [Dappier Marketplace](https://marketplace.dappier.com/marketplace). + + +### AI Models + +| Function | Model | Description | Arguments | +|:---|:---|:---|:---| +| `real_time_web_search` | `am_01j06ytn18ejftedz6dyhz2b15` | Perform a real-time web search across Google, news, weather, and travel data. | `query: str` | +| `stock_market_data_search` | `am_01j749h8pbf7ns8r1bq9s2evrh` | Perform a real-time stock market data search including stock prices and financial news. | `query: str` | + +### Data Models + +| Function | Model | Description | Arguments | +|:---|:---|:---|:---| +| `get_sports_news` | `dm_01j0pb465keqmatq9k83dthx34` | Get real-time sports news and updates from top sports sources. | `query: str`, `similarity_top_k: int`, `ref: Optional[str]`, `num_articles_ref: int`, `search_algorithm: Literal["most_recent", "semantic", "most_recent_semantic", "trending"]` | +| `get_lifestyle_news` | `dm_01j0q82s4bfjmsqkhs3ywm3x6y` | Access real-time lifestyle news and insights from popular publications. | `query: str`, `similarity_top_k: int`, `ref: Optional[str]`, `num_articles_ref: int`, `search_algorithm: Literal["most_recent", "semantic", "most_recent_semantic", "trending"]` | +| `get_iheartdogs_content` | `dm_01j1sz8t3qe6v9g8ad102kvmqn` | Fetch dog care articles on health, behavior, and grooming from iHeartDogs. | `query: str`, `similarity_top_k: int`, `ref: Optional[str]`, `num_articles_ref: int`, `search_algorithm: Literal["most_recent", "semantic", "most_recent_semantic", "trending"]` | +| `get_iheartcats_content` | `dm_01j1sza0h7ekhaecys2p3y0vmj` | Fetch cat care content on health, lifestyle, and behavior from iHeartCats. | `query: str`, `similarity_top_k: int`, `ref: Optional[str]`, `num_articles_ref: int`, `search_algorithm: Literal["most_recent", "semantic", "most_recent_semantic", "trending"]` | +| `get_greenmonster_guides` | `dm_01j5xy9w5sf49bm6b1prm80m27` | Access eco-conscious lifestyle articles from GreenMonster. | `query: str`, `similarity_top_k: int`, `ref: Optional[str]`, `num_articles_ref: int`, `search_algorithm: Literal["most_recent", "semantic", "most_recent_semantic", "trending"]` | +| `get_wishtv_news` | `dm_01jagy9nqaeer9hxx8z1sk1jx6` | Get news updates on politics, entertainment, and multicultural topics from WISH-TV. | `query: str`, `similarity_top_k: int`, `ref: Optional[str]`, `num_articles_ref: int`, `search_algorithm: Literal["most_recent", "semantic", "most_recent_semantic", "trending"]` | + +## Installation + +```bash +agentstack tools add dappier +``` + +Set the environment variable + +```env +DAPPIER_API_KEY=... +``` + +## Usage +Dappier can be configured for different behaviors by modifying `src/tools/dappier_tool.py`. diff --git a/docs/tools/tool/hyperbrowser.mdx b/docs/tools/tool/hyperbrowser.mdx new file mode 100644 index 00000000..ac4ce046 --- /dev/null +++ b/docs/tools/tool/hyperbrowser.mdx @@ -0,0 +1,132 @@ +--- +title: Hyperbrowser +description: AI-powered web browser automation and content extraction +icon: browser +--- + +## Description + +Hyperbrowser enables your agents to interact with the web through powerful browser automation. This tool allows agents to extract content from webpages, crawl entire sites, extract structured data, and perform sophisticated browser automation tasks using multiple AI agent options. + +## Example + +Add the Hyperbrowser tool with + +```bash +agentstack tools add hyperbrowser +``` + +Set up your environment variables: + +```env +HYPERBROWSER_API_KEY=your_api_key_here +``` + +## Features + +### Web Scraping + +Extract content from individual webpages in various formats: + +```python +from agentstack._tools.hyperbrowser import scrape_webpage + +result = scrape_webpage( + url="https://example.com", + use_proxy=True, + formats=["markdown"] # Options: markdown, html, links, screenshot +) +``` + +### Website Crawling + +Crawl entire websites and collect content from multiple pages: + +```python +from agentstack._tools.hyperbrowser import crawl_website + +result = crawl_website( + starting_url="https://example.com", + max_pages=10, + include_pattern=["/blog/*"], + exclude_pattern=["/admin/*"], + use_proxy=True +) +``` + +### Structured Data Extraction + +Extract data in structured format based on a schema and prompt: + +```python +from agentstack._tools.hyperbrowser import extract_data_from_webpages + +result = extract_data_from_webpages( + urls=["https://example.com/product"], + schema="{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"price\":{\"type\":\"string\"}}}", + prompt="Extract the product name and price from this page", + system_prompt="You are an expert at extracting product information", + use_proxy=True +) +``` + +### Browser Use Agent + +Run a fast, efficient browser automation agent for explicit instructions: + +```python +from agentstack._tools.hyperbrowser import run_browser_use_agent + +result = run_browser_use_agent( + task="Go to example.com and click on the first link", + max_steps=10, + use_vision=False, + use_vision_for_planner=False, + use_proxy=True +) +``` + +### Claude Computer Use Agent + +Leverage Claude's advanced reasoning capabilities for complex web tasks: + +```python +from agentstack._tools.hyperbrowser import run_claude_computer_use_agent + +result = run_claude_computer_use_agent( + task="Research the top 3 AI frameworks and summarize their features", + max_steps=20, + use_vision=True, + use_vision_for_planner=True, + use_proxy=True +) +``` + +### OpenAI Computer Use Agent + +Utilize OpenAI's balanced performance for general-purpose browser automation: + +```python +from agentstack._tools.hyperbrowser import run_openai_cua_agent + +result = run_openai_cua_agent( + task="Find the latest news on AI development", + max_steps=15, + use_vision=True, + use_vision_for_planner=False, + use_proxy=True +) +``` + +## Available Functions + +The Hyperbrowser tool provides the following core functions: + +- `scrape_webpage()`: Extract content from a single webpage in various formats +- `crawl_website()`: Collect content from multiple pages across a website +- `extract_data_from_webpages()`: Extract structured data from webpages based on a schema +- `run_browser_use_agent()`: Fast, lightweight agent for explicit browser automation tasks +- `run_claude_computer_use_agent()`: Claude-powered agent for complex reasoning tasks +- `run_openai_cua_agent()`: OpenAI-powered agent for general-purpose browser automation + +For detailed function parameters and usage, refer to the function docstrings in your IDE or the [Hyperbrowser documentation](https://docs.hyperbrowser.ai/). diff --git a/docs/tools/tool/hyperspell.mdx b/docs/tools/tool/hyperspell.mdx new file mode 100644 index 00000000..cbbd9465 --- /dev/null +++ b/docs/tools/tool/hyperspell.mdx @@ -0,0 +1,125 @@ +--- +title: Hyperspell +description: Let agents search, answer, and learn from internal data +--- + + +Hyperspell lets agents search, answer, and learn from internal knowledge. It works across uploaded documents, integrations like Notion, Gmail, and Slack, and raw text input. It provides fast semantic retrieval, optional question-answering, and user-specific permissions. Let your users connect to multiple sources and use Hyperspell’s end-to-end data pipeline to get structured data with a single API call. + +## Description + +[Hyperspell](https://hyperspell.com) provides agents with the ability to build and query internal knowledge using: + +- Cross-source search across documents, emails, Notion, Gmail, Slack, and many other integrations +- Upload files and documents to create your agents' memory +- Answering questions requiring multiple documents + +## Installation + +```bash +agentstack tools add hyperspell +``` + +Create your Hyperspell app and get your API key from the [Hyperspell Dashboard](https://app.hyperspell.com/dashboard). + +Follow the instructions to connect your data sources + +Set the environment variables in your project's `.env` file: + +```env +HYPERSPELL_API_KEY=your_api_key_here +``` + + + Hyperspell is a multi-tenant platform, and you can separate your data by use by setting the user id of your end-user in a variable too: + + ```env + HYPERSPELL_USER_ID=your_user_id_here + ``` + + + +## Available Functions + +The Hyperspell tool provides three core functions for knowledge retrieval and ingestion: + +### Query documents + +```python +hyperspell_search(query, sources, answer=False, user_id=None) +``` + +Search across your Hyperspell-connected sources for relevant information. + +**Parameters:** + + + The search query to find relevant information + + + Comma-separated list of sources to search (e.g., `collections,notion,gmail`) + + + If True, returns a direct AI answer instead of just documents. Defaults to False + + + User ID to use for this request. Defaults to `HYPERSPELL_USER_ID` env var + + +### Add documents + +```python +hyperspell_add_document(text, title=None, collection=None, user_id=None) +``` + +Add a text document to your Hyperspell knowledge base. + +**Parameters:** + + + The full text content to add + + + Optional title for the document + + + Optional collection name to organize the document + + + User ID to use for this request. Defaults to `HYPERSPELL_USER_ID` env var + + + +### Upload files + +```python +hyperspell_upload_file(file_path, collection=None, user_id=None) +``` + +Upload a file (PDF, Word doc, spreadsheet, etc.) to your Hyperspell knowledge base. + +**Parameters:** + + + Path to the file to upload + + + Optional collection name to organize the document + + + User ID to use for this request. Defaults to `HYPERSPELL_USER_ID` env var + + +If no `user_id` is provided, the tool defaults to the `HYPERSPELL_USER_ID` environment variable. + +## Integration Sources + +Hyperspell can search across multiple integrated sources: + +- **Collections**: Your uploaded documents and added text +- **Notion**: Connected Notion workspaces +- **Gmail**: Connected Gmail accounts +- **Slack**: Connected Slack workspaces +- **and many more**: Additional integrations available through the Hyperspell platform + +For detailed integration setup, visit the [Hyperspell documentation](https://docs.hyperspell.com). diff --git a/examples/market_monitoring/.env.example b/examples/market_monitoring/.env.example new file mode 100644 index 00000000..24234ffb --- /dev/null +++ b/examples/market_monitoring/.env.example @@ -0,0 +1,5 @@ +#AGENTOPS_API_KEY=... +#OPENAI_API_KEY=... + +# Tools +AGENTQL_API_KEY=... \ No newline at end of file diff --git a/examples/market_monitoring/.gitignore b/examples/market_monitoring/.gitignore new file mode 100644 index 00000000..7105da50 --- /dev/null +++ b/examples/market_monitoring/.gitignore @@ -0,0 +1,166 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +.agentops/ +agentstack.log +.agentstack* \ No newline at end of file diff --git a/examples/market_monitoring/LICENSE.md b/examples/market_monitoring/LICENSE.md new file mode 100644 index 00000000..41752f3b --- /dev/null +++ b/examples/market_monitoring/LICENSE.md @@ -0,0 +1,10 @@ + +MIT License + +Copyright (c) 2025 Name + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/examples/market_monitoring/README.md b/examples/market_monitoring/README.md new file mode 100644 index 00000000..0366544e --- /dev/null +++ b/examples/market_monitoring/README.md @@ -0,0 +1,46 @@ +# market_monitoring + +Tracks competitor websites for pricing and product updates. + +## How to Build this Project + +### With the CLI + +```bash +agentstack init market_monitoring + +agentstack generate agent web_scraper +agentstack generate task scrape_site + +agentstack generate agent market_reporter +agentstack generate task report + +agentstack tools add agentql +``` + +Add more agents with `agentstack agent ` and more tasks with `agentstack task ` + +Add tools with `agentstack tools add ` and view tools available with `agentstack tools list` + +## How to use your Agent + +In this directory, run `uv pip install --requirements pyproject.toml` + +To run your project, use the following command: +`agentstack run` + +This will initialize your crew of AI agents and begin task execution as defined in your configuration in the main.py file. + +#### Replay Tasks from Latest Crew Kickoff: + +CrewAI now includes a replay feature that allows you to list the tasks from the last run and replay from a specific one. To use this feature, run: +`crewai replay ` +Replace with the ID of the task you want to replay. + +#### Reset Crew Memory + +If you need to reset the memory of your crew before running it again, you can do so by calling the reset memory feature: +`crewai reset-memory` +This will clear the crew's memory, allowing for a fresh start. + +> 🪩 Project built with [AgentStack](https://github.com/AgentOps-AI/AgentStack) diff --git a/examples/market_monitoring/agentstack.json b/examples/market_monitoring/agentstack.json new file mode 100644 index 00000000..cef7dc11 --- /dev/null +++ b/examples/market_monitoring/agentstack.json @@ -0,0 +1,11 @@ +{ + "framework": "crewai", + "tools": [ + "agentql" + ], + "default_model": "openai/gpt-4o", + "agentstack_version": "0.3.5", + "template": "market_monitoring", + "template_version": "4", + "use_git": false +} \ No newline at end of file diff --git a/examples/market_monitoring/pyproject.toml b/examples/market_monitoring/pyproject.toml new file mode 100644 index 00000000..ee38e178 --- /dev/null +++ b/examples/market_monitoring/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "market_monitoring" +version = "0.0.1" +description = "" +authors = [ + { name = "Name " } +] +license = { text = "MIT" } +requires-python = ">=3.10" + +dependencies = [ + "agentstack[crewai]>=0.3.5", +] \ No newline at end of file diff --git a/examples/market_monitoring/src/__init__.py b/examples/market_monitoring/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/market_monitoring/src/config/agents.yaml b/examples/market_monitoring/src/config/agents.yaml new file mode 100644 index 00000000..fec5e257 --- /dev/null +++ b/examples/market_monitoring/src/config/agents.yaml @@ -0,0 +1,16 @@ +web_scraper: + role: >- + web scraper + goal: >- + Access the website and extract all relevant information about the product in a structured format. + backstory: >- + You are an expert web scraper and data extractor. + llm: openai/gpt-4o +market_reporter: + role: >- + market monitoring reporter + goal: >- + Track competitor websites for pricing and product updates, delivering insights into the dashboard. + backstory: >- + You are an expert in reporting and tracking competitor's websites. + llm: openai/gpt-4o diff --git a/examples/market_monitoring/src/config/inputs.yaml b/examples/market_monitoring/src/config/inputs.yaml new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/market_monitoring/src/config/inputs.yaml @@ -0,0 +1 @@ + diff --git a/examples/market_monitoring/src/config/tasks.yaml b/examples/market_monitoring/src/config/tasks.yaml new file mode 100644 index 00000000..62f7a78e --- /dev/null +++ b/examples/market_monitoring/src/config/tasks.yaml @@ -0,0 +1,16 @@ +scrape_site: + description: >- + Extract all the data about each electronic product from the following URL links: + https://www.amazon.com/gp/browse.html?rw_useCurrentProtocol=1&node=565108&ref_=amb_link_BpW_pJfGS-SH8sCy2LOykw_2 + https://www.bestbuy.com/site/computers-pcs/laptop-computers/abcat0502000.c?id=abcat0502000 + expected_output: >- + A json file. + agent: >- + web_scraper +report: + description: >- + Compare products and their prices between the competitor websites and give a report on their performances. + expected_output: >- + A few paragraphs about the market products. + agent: >- + market_reporter diff --git a/examples/market_monitoring/src/crew.py b/examples/market_monitoring/src/crew.py new file mode 100644 index 00000000..f3abf1d1 --- /dev/null +++ b/examples/market_monitoring/src/crew.py @@ -0,0 +1,48 @@ +from crewai import Agent, Crew, Process, Task +from crewai.project import CrewBase, agent, crew, task +import agentstack + + +@CrewBase +class MarketmonitoringCrew: + """market_monitoring crew""" + + @agent + def web_scraper(self) -> Agent: + return Agent( + config=self.agents_config["web_scraper"], + tools=[ + *agentstack.tools["agentql"] + ], # add tools here or use `agentstack tools add + verbose=True, + ) + + @agent + def market_reporter(self) -> Agent: + return Agent( + config=self.agents_config["market_reporter"], + tools=[], # add tools here or use `agentstack tools add + verbose=True, + ) + + @task + def scrape_site(self) -> Task: + return Task( + config=self.tasks_config["scrape_site"], + ) + + @task + def report(self) -> Task: + return Task( + config=self.tasks_config["report"], + ) + + @crew + def crew(self) -> Crew: + """Creates the Test crew""" + return Crew( + agents=self.agents, # Automatically created by the @agent decorator + tasks=self.tasks, # Automatically created by the @task decorator + process=Process.sequential, + verbose=True, + ) diff --git a/examples/market_monitoring/src/main.py b/examples/market_monitoring/src/main.py new file mode 100644 index 00000000..f7b05ebb --- /dev/null +++ b/examples/market_monitoring/src/main.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +import sys +from crew import MarketmonitoringCrew +import agentstack +import agentops + +agentops.init(default_tags=agentstack.get_tags()) + +instance = MarketmonitoringCrew().crew() + +def run(): + """ + Run the agent. + """ + instance.kickoff(inputs=agentstack.get_inputs()) + + +def train(): + """ + Train the crew for a given number of iterations. + """ + try: + instance.train( + n_iterations=int(sys.argv[1]), + filename=sys.argv[2], + inputs=agentstack.get_inputs(), + ) + except Exception as e: + raise Exception(f"An error occurred while training the crew: {e}") + + +def replay(): + """ + Replay the crew execution from a specific task. + """ + try: + instance.replay(task_id=sys.argv[1]) + except Exception as e: + raise Exception(f"An error occurred while replaying the crew: {e}") + + +def test(): + """ + Test the crew execution and returns the results. + """ + try: + instance.test( + n_iterations=int(sys.argv[1]), + openai_model_name=sys.argv[2], + inputs=agentstack.get_inputs(), + ) + except Exception as e: + raise Exception(f"An error occurred while replaying the crew: {e}") + + +if __name__ == '__main__': + run() \ No newline at end of file diff --git a/examples/market_monitoring/src/tools/__init__.py b/examples/market_monitoring/src/tools/__init__.py new file mode 100644 index 00000000..ebbad834 --- /dev/null +++ b/examples/market_monitoring/src/tools/__init__.py @@ -0,0 +1,2 @@ + +# tool import \ No newline at end of file diff --git a/examples/research_assistant/README.md b/examples/research_assistant/README.md index bd31d632..4900c858 100644 --- a/examples/research_assistant/README.md +++ b/examples/research_assistant/README.md @@ -1,21 +1,33 @@ # research_assistant +Answers research questions by retrieving knowledge from the web and extracting key insights. + +## How to Build this Project -## How to build your Crew Agent ### With the CLI -Add an agent using AgentStack with the CLI: -`agentstack generate agent ` -You can also shorten this to `agentstack g a ` -For wizard support use `agentstack g a --wizard` -Finally for creation in the CLI alone, use `agentstack g a --role/-r --goal/-g --backstory/-b --model/-m ` -This will automatically create a new agent in the `agents.yaml` config as well as in your code. Either placeholder strings will be used, or data included in the wizard. +```bash +agentstack init research_assistant + +agentstack generate agent web_scraper +agentstack generate task scrape_site + +agentstack generate agent researcher +agentstack generate task research + +agentstack generate agent analyst +agentstack generate task analyze -Similarly, tasks can be created with `agentstack g t ` +agentstack tools add agentql +agentstack tools add firecrawl +``` -Add tools with `agentstack tools add` and view tools available with `agentstack tools list` +Add more agents with `agentstack agent ` and more tasks with `agentstack task ` + +Add tools with `agentstack tools add ` and view tools available with `agentstack tools list` ## How to use your Agent + In this directory, run `uv pip install --requirements pyproject.toml` To run your project, use the following command: @@ -30,8 +42,9 @@ CrewAI now includes a replay feature that allows you to list the tasks from the Replace with the ID of the task you want to replay. #### Reset Crew Memory + If you need to reset the memory of your crew before running it again, you can do so by calling the reset memory feature: `crewai reset-memory` This will clear the crew's memory, allowing for a fresh start. -> 🪩 Project built with [AgentStack](https://github.com/AgentOps-AI/AgentStack) \ No newline at end of file +> 🪩 Project built with [AgentStack](https://github.com/AgentOps-AI/AgentStack) diff --git a/examples/sentiment_analyser/README.md b/examples/sentiment_analyser/README.md index 1f41282e..4c527a82 100644 --- a/examples/sentiment_analyser/README.md +++ b/examples/sentiment_analyser/README.md @@ -1,22 +1,30 @@ # sentiment_analyser -This is the start of your AgentStack project. -## How to build your Crew +Conducts a sentimental analysis on a Reddit thread based on its comments. + +## How to Build this Project + ### With the CLI -Add an agent using AgentStack with the CLI: -`agentstack generate agent ` -You can also shorten this to `agentstack g a ` -For wizard support use `agentstack g a --wizard` -Finally for creation in the CLI alone, use `agentstack g a --role/-r --goal/-g --backstory/-b --model/-m ` -This will automatically create a new agent in the `agents.yaml` config as well as in your code. Either placeholder strings will be used, or data included in the wizard. +```bash +agentstack init sentiment_analyser + +agentstack generate agent web_scraper +agentstack generate task scrape_data + +agentstack generate agent analyser +agentstack generate task sentiment_analysis -Similarly, tasks can be created with `agentstack g t ` +agentstack tools add agentql +``` -Add tools with `agentstack tools add` and view tools available with `agentstack tools list` +Add more agents with `agentstack agent ` and more tasks with `agentstack task ` + +Add tools with `agentstack tools add ` and view tools available with `agentstack tools list` ## How to use your Crew -In this directory, run `poetry install` + +In this directory, run `poetry install` To run your project, use the following command: `agentstack run` @@ -30,8 +38,9 @@ CrewAI now includes a replay feature that allows you to list the tasks from the Replace with the ID of the task you want to replay. #### Reset Crew Memory + If you need to reset the memory of your crew before running it again, you can do so by calling the reset memory feature: `crewai reset-memory` This will clear the crew's memory, allowing for a fresh start. -> 🪩 Project built with [AgentStack](https://github.com/AgentOps-AI/AgentStack) \ No newline at end of file +> 🪩 Project built with [AgentStack](https://github.com/AgentOps-AI/AgentStack) diff --git a/examples/stock_market_research/.env.example b/examples/stock_market_research/.env.example new file mode 100644 index 00000000..4272c015 --- /dev/null +++ b/examples/stock_market_research/.env.example @@ -0,0 +1,6 @@ +#AGENTOPS_API_KEY=... +#OPENAI_API_KEY=... + +# Tools + +#DAPPIER_API_KEY=... diff --git a/examples/stock_market_research/.gitignore b/examples/stock_market_research/.gitignore new file mode 100644 index 00000000..7105da50 --- /dev/null +++ b/examples/stock_market_research/.gitignore @@ -0,0 +1,166 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +.agentops/ +agentstack.log +.agentstack* \ No newline at end of file diff --git a/examples/stock_market_research/LICENSE.md b/examples/stock_market_research/LICENSE.md new file mode 100644 index 00000000..41752f3b --- /dev/null +++ b/examples/stock_market_research/LICENSE.md @@ -0,0 +1,10 @@ + +MIT License + +Copyright (c) 2025 Name + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/examples/stock_market_research/README.md b/examples/stock_market_research/README.md new file mode 100644 index 00000000..118a6a41 --- /dev/null +++ b/examples/stock_market_research/README.md @@ -0,0 +1,37 @@ +# stock_market_research + + +## How to build your Crew Agent +### With the CLI +Add an agent using AgentStack with the CLI: +`agentstack generate agent ` +You can also shorten this to `agentstack g a ` +For wizard support use `agentstack g a --wizard` +Finally for creation in the CLI alone, use `agentstack g a --role/-r --goal/-g --backstory/-b --model/-m ` + +This will automatically create a new agent in the `agents.yaml` config as well as in your code. Either placeholder strings will be used, or data included in the wizard. + +Similarly, tasks can be created with `agentstack g t ` + +Add tools with `agentstack tools add` and view tools available with `agentstack tools list` + +## How to use your Agent +In this directory, run `uv pip install --requirements pyproject.toml` + +To run your project, use the following command: +`agentstack run` + +This will initialize your crew of AI agents and begin task execution as defined in your configuration in the main.py file. + +#### Replay Tasks from Latest Crew Kickoff: + +CrewAI now includes a replay feature that allows you to list the tasks from the last run and replay from a specific one. To use this feature, run: +`crewai replay ` +Replace with the ID of the task you want to replay. + +#### Reset Crew Memory +If you need to reset the memory of your crew before running it again, you can do so by calling the reset memory feature: +`crewai reset-memory` +This will clear the crew's memory, allowing for a fresh start. + +> 🪩 Project built with [AgentStack](https://github.com/AgentOps-AI/AgentStack) \ No newline at end of file diff --git a/examples/stock_market_research/agentstack.json b/examples/stock_market_research/agentstack.json new file mode 100644 index 00000000..dd52d7ba --- /dev/null +++ b/examples/stock_market_research/agentstack.json @@ -0,0 +1,11 @@ +{ + "framework": "crewai", + "tools": [ + "dappier" + ], + "default_model": "openai/gpt-4o", + "agentstack_version": "0.3.5", + "template": "stock_market_research", + "template_version": "4", + "use_git": false +} \ No newline at end of file diff --git a/examples/stock_market_research/pyproject.toml b/examples/stock_market_research/pyproject.toml new file mode 100644 index 00000000..5ead5007 --- /dev/null +++ b/examples/stock_market_research/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "stock_market_research" +version = "0.0.1" +description = "" +authors = [ + { name = "Name " } +] +license = { text = "MIT" } +requires-python = ">=3.10" + +dependencies = [ + "agentstack>=0.3.5", + "crewai>=0.118.0", + "dappier>=0.3.5", +] diff --git a/examples/stock_market_research/reports/tesla_investment_report.md b/examples/stock_market_research/reports/tesla_investment_report.md new file mode 100644 index 00000000..8c08b366 --- /dev/null +++ b/examples/stock_market_research/reports/tesla_investment_report.md @@ -0,0 +1,84 @@ +# Investment Report: Tesla, Inc. (TSLA) + +## 1. AI Summary of Tesla + +Tesla is a leading force in the automotive industry, particularly in electric vehicles, under the leadership of CEO Elon Musk. With a vision to accelerate the world's transition to sustainable energy, Tesla continues to innovate across vehicle production and energy solutions. Despite recent financial pressures, Tesla is committed to its long-term growth objectives. + +## 2. Company Profile + +- **Industry/Sector**: Automotive / Electric Vehicles +- **CEO**: Elon Musk +- **Headquarters Location**: Palo Alto, California, USA +- **Employee Count**: Approximately 125,665 +- **Market Capitalization**: Around $921 billion +- **Stock Ticker Symbol**: TSLA + +## 3. Financial Performance Metrics + +| Metric | Value | +|-----------------------------|--------------------------| +| **Revenue (TTM)** | $95.72 billion | +| **Net Income (TTM)** | $1.5 billion | +| **YoY Revenue Growth** | -9.20% | +| **Gross Margin** | 16.3% | +| **Operating Income** | $400 million | +| **Operating Margin** | 2.1% | +| **Q1 2025 Highlights** | Significant margin pressures; Energy storage deployments up 154% YoY | + +## 4. Competitive Benchmarking + +| Company | Market Cap (B) | Stock Price | P/E Ratio | Revenue (TTM) (B) | +|---------------|----------------|-------------|-----------|-------------------| +| **Tesla, Inc.** (TSLA) | $921 | ~$220 | ~60 | $95.72 | +| **NIO Inc.** (NIO) | $30 | ~$12 | ~25 | $6.5 | +| **Rivian Automotive, Inc.** (RIVN) | $18 | ~$10 | N/A (losses) | $1.5 | +| **Lucid Motors, Inc.** (LCID) | $12 | ~$8 | N/A (losses) | $1 | +| **Li Auto Inc.** (LI) | $20 | ~$15 | ~30 | $5 | + +### Key Highlights: +- Tesla's market cap and revenue far exceed those of its peers. +- High P/E ratio suggests strong investor confidence in future growth. + +## 5. Real-Time Stock Snapshot + +- **Current Price:** $282.86 +- **Daily Change:** +$3.21 (+1.15%) +- **Volume:** 1,500,000 shares +- **52-Week High:** $350.00 +- **52-Week Low:** $180.00 +- **P/E Ratio:** 45.67 +- **EPS:** $6.19 +- **Dividend Yield:** 0.00% + +### Price Performance: +- **1 Day:** +1.15% +- **5 Days:** +3.50% +- **1 Month:** +5.00% +- **YTD:** +15.00% +- **1 Year:** +25.00% + +## 6. Categorized Financial News + +### Market Moves: +- **Unfortunate News for Tesla Stock Investors** (Sentiment: Negative) - Reporting on increased tariffs impacting costs and likely reducing margins. [Read more](https://www.fool.com/investing/2025/05/01/unfortunate-news-for-tesla-stock-investors/?source=iedfolrf0000001) + +### Partnerships: +- **Why Tesla Stock Hit the Brakes Today** (Sentiment: Cautious) - Analysis on the potential impact of a new Waymo-Toyota partnership on Tesla's autonomous driving strategies. [Read more](https://www.fool.com/investing/2025/04/30/why-tesla-stock-hit-the-brakes-today/?source=iedfolrf0000001) + +## 7. Insight Section + +### What's Going on with Tesla + +Tesla is facing margin pressures due to increased costs associated with tariffs and inventory management challenges. Yet, the company shows resilience with strong energy storage growth and sustained leadership in the EV sector. + +### Why It Matters + +These financial and market challenges are pivotal as they may influence Tesla's global competitiveness, investor sentiment, and stock performance. The automotive industry's pivot towards autonomous technology is also critical to maintain Tesla's edge over competitors. + +### Outlook (Not Financial Advice) + +Despite current challenges, Tesla’s innovative approach and substantial market presence suggest potential for recovery and growth, especially with strategic advancements in energy and technology. Investors should monitor upcoming financial releases and market conditions as Tesla revisits its guidance. + +``` + +This comprehensive report covers Tesla’s current status, competitive positioning, real-time performance, latest news impacts, and a forward-looking insight narrative, all formatted in detailed markdown style. \ No newline at end of file diff --git a/examples/stock_market_research/src/__init__.py b/examples/stock_market_research/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/stock_market_research/src/config/agents.yaml b/examples/stock_market_research/src/config/agents.yaml new file mode 100644 index 00000000..ea5df986 --- /dev/null +++ b/examples/stock_market_research/src/config/agents.yaml @@ -0,0 +1,44 @@ +web_researcher: + role: >- + A company research analyst that collects structured business data, financials, + and competitive insights from the dappier real time web search. + goal: >- + To compile detailed company profiles using real-time data, covering company overview, + financial performance, and peer benchmarking. + backstory: >- + Trained to support investment research workflows, this agent uses Dappier’s real-time + web search to gather trustworthy and current business information. It builds company + snapshots with industry, CEO, market cap, and financial metrics like revenue and + net income. It also auto-compares the company to peers based on valuation and + performance metrics. + llm: openai/gpt-4o +stock_insights_analyst: + role: >- + A stock market intelligence analyst that retrieves real-time financial data and + curated news using the dappier stock market data search. + goal: >- + To deliver up-to-date stock snapshots, performance metrics, and categorized financial + news for informed investment analysis. + backstory: >- + Trained to analyze real-time financial markets using Dappier’s stock market data + tool, this agent specializes in stock-specific queries. It always includes a valid + natural language query with stock ticker symbol before sending requests. The agent + provides live insights into stock price movements, valuation ratios, earnings, and + sentiment-tagged news from reliable financial feeds like Polygon.io. + llm: openai/gpt-4o +report_analyst: + role: >- + A financial report analyst that consolidates real-time stock and company insights + into a comprehensive markdown report. + goal: >- + To generate an investor-facing, markdown-formatted summary combining company profile, + financials, benchmarking, stock performance, and real-time news with actionable + insights. + backstory: >- + Specialized in synthesizing structured data retrieved by other research agents, + this agent produces detailed markdown reports that explain what's happening with + a given stock ticker, why it matters, and what the short-term outlook may be. + It uses both company-level and stock-level intelligence—ensuring all information + is up-to-date and grounded in real-time data. Forecasts are AI-generated and clearly + marked as non-financial advice. + llm: openai/gpt-4o diff --git a/examples/stock_market_research/src/config/inputs.yaml b/examples/stock_market_research/src/config/inputs.yaml new file mode 100644 index 00000000..69e8d3bc --- /dev/null +++ b/examples/stock_market_research/src/config/inputs.yaml @@ -0,0 +1 @@ +company_name: tesla diff --git a/examples/stock_market_research/src/config/tasks.yaml b/examples/stock_market_research/src/config/tasks.yaml new file mode 100644 index 00000000..0429f939 --- /dev/null +++ b/examples/stock_market_research/src/config/tasks.yaml @@ -0,0 +1,85 @@ +company_overview: + description: >- + As of {timestamp}, fetch the company overview for {company_name} using real-time web search with the timestamp. Include + company profile, industry, sector, CEO, headquarters location, employee count, market + capitalization and stock ticker symbol. + expected_output: >- + A structured company profile including: Company Profile, Industry, Sector, CEO, + HQ Location, Employees, Market Cap and the stock ticker symbol. + agent: >- + web_researcher + +financials_performance: + description: >- + As of {timestamp}, use real-time web search with the timestamp to extract financial performance data for {company_name}, + including Revenue (TTM), Net Income (TTM), Year-over-Year revenue growth, gross + margin, and recent quarterly trends. Include any earnings trends or management + commentary available. + expected_output: >- + A structured summary of financial metrics for {company_name}: Revenue (TTM), Net + Income (TTM), YoY Revenue Growth, Gross Margin, Quarterly Trends, and Earnings + Commentary. + agent: >- + web_researcher + +competitive_benchmarking: + description: >- + As of {timestamp}, perform real-time web search with the timestamp to identify 3-5 peer companies in the same sector + as {company_name}. Extract and compare key metrics such as P/E ratio, revenue, + stock price, and market cap. Highlight any standout metrics where {company_name} + outperforms or underperforms. + expected_output: >- + A comparison table of {company_name} and 3-5 peers showing P/E, revenue, price, + and market cap. Highlight metrics where {company_name} stands out. + agent: >- + web_researcher + +real_time_stock_snapshot: + description: >- + As of {timestamp}, convert {company_name} to its stock ticker symbol and retrieve a real-time stock + snapshot using Dappier’s stock market data tool with the timestamp. Include current price with % + daily change, volume, 52-week high/low, P/E ratio, EPS, dividend yield, and chart + data for 1D, 5D, 1M, YTD, and 1Y in the query. + expected_output: >- + A structured stock summary for {company_name}, including: + Price, % Daily Change, Volume, 52-Week High/Low, P/E Ratio, EPS, Dividend Yield, + and chart data for 1D, 5D, 1M, YTD, 1Y. + agent: >- + stock_insights_analyst + +news_and_sentiment: + description: >- + As of {timestamp}, convert {company_name} to its stock ticker symbol and fetch a real-time financial + news stream using Dappier’s stock market data tool with the timestamp. Categorize the news by topic: + Earnings, Analyst Ratings, Market Moves, Partnerships, and Legal/Regulatory in the + query. + expected_output: >- + A categorized list of real-time financial news headlines + for {company_name}, organized by topic: Earnings, Analyst + Ratings, Market Moves, Partnerships, Legal/Regulatory in the query. + agent: >- + stock_insights_analyst + +generate_investment_report: + description: >- + As of {timestamp}, compile a comprehensive, markdown-formatted investment report + for {company_name} by synthesizing the outputs of all prior tasks: company overview, + financial performance, competitive benchmarking, real-time stock snapshot, and + categorized financial news. Use the timestamp in all queries. Include a concise AI-generated company summary, + structured data tables, sentiment-tagged news, and a narrative insight section. + expected_output: >- + A markdown-formatted investment report containing: + 1. Quick AI summary of {company_name} (e.g., "Apple is a global tech leader…") + 2. Structured company profile: Industry, Sector, CEO, HQ, Employees, Market Cap + 3. Financial performance metrics: Revenue (TTM), Net Income (TTM), YoY Growth, Gross Margin, Trends + 4. Competitive benchmarking table: P/E, Revenue, Stock Price, Market Cap vs. 3–5 peers + 5. Real-time stock snapshot: Price, % Change, Volume, 52W High/Low, P/E, EPS, Dividend, charts + 6. Categorized news: Earnings, Analyst Ratings, Market Moves, Partnerships, Legal/Regulatory (with sentiment tags) + 7. Final 3-part insight section: + - What's going on with {company_name} + - Why it matters + - Outlook (clearly marked as not financial advice) + agent: >- + report_analyst + output_file: reports/{company_name}_investment_report.md + create_directory: true diff --git a/examples/stock_market_research/src/crew.py b/examples/stock_market_research/src/crew.py new file mode 100644 index 00000000..6135a440 --- /dev/null +++ b/examples/stock_market_research/src/crew.py @@ -0,0 +1,87 @@ +from crewai import Agent, Crew, Process, Task +from crewai.project import CrewBase, agent, crew, task +import agentstack + +@CrewBase +class StockmarketresearchCrew(): + """stock_market_research crew""" + + @agent + def web_researcher(self) -> Agent: + return Agent( + config=self.agents_config['web_researcher'], + tools = [ + get_dappier_tool("real_time_web_search") + ], + verbose=True, + ) + + @agent + def stock_insights_analyst(self) -> Agent: + return Agent( + config=self.agents_config['stock_insights_analyst'], + tools = [ + get_dappier_tool("stock_market_data_search") + ], + verbose=True, + ) + + @agent + def report_analyst(self) -> Agent: + return Agent( + config=self.agents_config['report_analyst'], + verbose=True, + ) + + @task + def company_overview(self) -> Task: + return Task( + config=self.tasks_config['company_overview'], + ) + + @task + def financials_performance(self) -> Task: + return Task( + config=self.tasks_config['financials_performance'], + ) + + @task + def competitive_benchmarking(self) -> Task: + return Task( + config=self.tasks_config['competitive_benchmarking'], + ) + + @task + def real_time_stock_snapshot(self) -> Task: + return Task( + config=self.tasks_config['real_time_stock_snapshot'], + ) + + @task + def news_and_sentiment(self) -> Task: + return Task( + config=self.tasks_config['news_and_sentiment'], + ) + + @task + def generate_investment_report(self) -> Task: + return Task( + config=self.tasks_config['generate_investment_report'], + ) + + @crew + def crew(self) -> Crew: + """Creates the Test crew""" + return Crew( + agents=self.agents, # Automatically created by the @agent decorator + tasks=self.tasks, # Automatically created by the @task decorator + process=Process.sequential, + verbose=True, + ) + + +def get_dappier_tool(tool_name: str): + for tool in agentstack.tools["dappier"]: + if tool.name == tool_name: + return tool + return None diff --git a/examples/stock_market_research/src/main.py b/examples/stock_market_research/src/main.py new file mode 100644 index 00000000..7be0e6c0 --- /dev/null +++ b/examples/stock_market_research/src/main.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +from datetime import datetime, timezone +import sys +from crew import StockmarketresearchCrew +import agentstack +import agentops + +agentops.init(default_tags=agentstack.get_tags()) + +instance = StockmarketresearchCrew().crew() + +def run(): + """ + Run the agent. + """ + inputs = agentstack.get_inputs() + inputs["timestamp"] = datetime.now(timezone.utc).isoformat() + + instance.kickoff(inputs=inputs) + + +def train(): + """ + Train the crew for a given number of iterations. + """ + try: + instance.train( + n_iterations=int(sys.argv[1]), + filename=sys.argv[2], + inputs=agentstack.get_inputs(), + ) + except Exception as e: + raise Exception(f"An error occurred while training the crew: {e}") + + +def replay(): + """ + Replay the crew execution from a specific task. + """ + try: + instance.replay(task_id=sys.argv[1]) + except Exception as e: + raise Exception(f"An error occurred while replaying the crew: {e}") + + +def test(): + """ + Test the crew execution and returns the results. + """ + try: + instance.test( + n_iterations=int(sys.argv[1]), + openai_model_name=sys.argv[2], + inputs=agentstack.get_inputs(), + ) + except Exception as e: + raise Exception(f"An error occurred while replaying the crew: {e}") + + +if __name__ == '__main__': + run() \ No newline at end of file diff --git a/examples/stock_market_research/src/tools/__init__.py b/examples/stock_market_research/src/tools/__init__.py new file mode 100644 index 00000000..ebbad834 --- /dev/null +++ b/examples/stock_market_research/src/tools/__init__.py @@ -0,0 +1,2 @@ + +# tool import \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..f6f8a8ad --- /dev/null +++ b/install.sh @@ -0,0 +1,706 @@ +#!/bin/bash +export LANG=en_US.UTF-8 +set -e + +LOGO=$(cat <<'EOF' + ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ + /\ \ /\ \ /\ \ /\__\ /\ \ /\ \ /\ \ /\ \ /\ \ /\__\ + /::\ \ /::\ \ /::\ \ /:| _|_ \:\ \ /::\ \ \:\ \ /::\ \ /::\ \ /:/ _/_ + /::\:\__\ /:/\:\__\ /::\:\__\ /::|/\__\ /::\__\ /\:\:\__\ /::\__\ /::\:\__\ /:/\:\__\ /::- \__\\ + \/\::/ / \:\:\/__/ \:\:\/ / \/|::/ / /:/\/__/ \:\:\/__/ /:/\/__/ \/\::/ / \:\ \/__/ \;:;- ,- + /:/ / \::/ / \:\/ / |:/ / \/__/ \::/ / \/__/ /:/ / \:\__\ |:| | + \/__/ \/__/ \/__/ \/__/ \/__/ \/__/ \/__/ \|__| +EOF +) + +APP_NAME="agentstack" +VERSION="0.3.5" +REPO_URL="https://github.com/AgentOps-AI/AgentStack" +RELEASE_PATH_URL="$REPO_URL/archive/refs/tags" +CHECKSUM_URL="" # TODO +PYTHON_VERSION=">=3.10,<3.13" +UV_INSTALLER_URL="https://astral.sh/uv/install.sh" +CACHE_DIR="$HOME/.cache" +PYTHON_BIN_PATH="" # set after a verified install is found +DEV_BRANCH="" # set by --dev-branch flag +DO_UNINSTALL=0 # set by uninstall flag +INIT_TEMPLATE="" +INIT_NAME="" +PRINT_VERBOSE=0 +PRINT_QUIET=1 + +MSG_SUCCESS=$(cat < Specify version to install (default: $VERSION) + --python-version= Specify Python version to install (default: $PYTHON_VERSION) + --dev-branch= Install from a specific git branch/commit/tag + --verbose Enable verbose output + --quiet Suppress output + -h, --help Show this help message +EOF +} + +say() { + if [ "1" = "$PRINT_QUIET" ]; then + echo "$1" + fi +} + +say_verbose() { + if [ "1" = "$PRINT_VERBOSE" ]; then + echo "[DEBUG] $1" + fi +} + +ACTIVITY_PID="" +_show_activity() { + while true; do + echo -n "." + sleep 1 + done +} + +show_activity() { + if [ "0" = "$PRINT_QUIET" ] || [ "1" = "$PRINT_VERBOSE" ]; then + return 0 + fi + _show_activity & + ACTIVITY_PID=$! + # trap end_activity EXIT + # trap 'kill $ACTIVITY_PID' INT + # wait $ACTIVITY_PID +} + +end_activity() { + if [ -n "$ACTIVITY_PID" ]; then + say "" # newline after the dots + kill $ACTIVITY_PID + fi +} + +err() { + end_activity + if [ "1" = "$PRINT_QUIET" ]; then + local _red=$(tput setaf 1 2>/dev/null || echo '') + local _reset=$(tput sgr0 2>/dev/null || echo '') + say "" + say "${_red}[ERROR]${_reset}: $1" >&2 + say "" + say "Run with --verbose for more details." + say "" + say "If you need help, please feel free to open an issue:" + say " $REPO_URL/issues" + say "" + say "Or, try an alternate installation method at:" + say " https://docs.agentstack.sh/installation" + say "" + fi + exit 1 +} + +err_missing_cmd() { + local _cmd_name=$1 + local _help_text="" + local _platform=$(platform) + + if [ $_platform == "linux" ]; then + if [ $_cmd_name == "gcc" ]; then + _help_text="Hint: sudo apt-get install build-essential" + else + _help_text="Hint: sudo apt-get install $_cmd_name" + fi + elif [ $_platform == "macos" ]; then + _help_text="Hint: brew install $_cmd_name" + fi + err "A required dependency is missing. Please install: $1 +$_help_text" +} + +# Check if a command exists +check_cmd() { + command -v "$1" > /dev/null 2>&1 + return $? +} + +# Check if a command exists and print an error message if it doesn't +need_cmd() { + if ! check_cmd "$1"; then + err_missing_cmd $1 + fi +} + +# Check if one of multiple commands exist and print an error message if none do +need_cmd_option() { + local _found=0 + for cmd in "$@"; do + if check_cmd "$cmd"; then + _found=1 + break + fi + done + + if [ $_found -eq 0 ]; then + err_missing_cmd $1 + fi +} + +ensure() { + if ! "$@"; then err "command failed: $*"; fi +} + +platform() { + case "$(uname -s)" in + Linux*) echo "linux" ;; + Darwin*) echo "macos" ;; + CYGWIN*) echo "cygwin" ;; + *) echo "unknown" ;; + esac +} + +# Check for required commands +check_dependencies() { + say "Checking dependencies..." + need_cmd mkdir + need_cmd mktemp + need_cmd chmod + need_cmd rm + need_cmd grep + need_cmd awk + need_cmd cat + + need_cmd_option curl wget + need_cmd_option tar unzip + need_cmd gcc # need gcc to install psutil + say "Dependencies are met." +} + +# Install uv +install_uv() { + if check_cmd uv; then + say_verbose "uv is already installed." + return 0 + else + say "Installing uv..." + fi + show_activity + + # download with curl or wget + local _install_cmd + if check_cmd curl; then + say_verbose "Running uv installer with curl" + _install_cmd="curl -LsSf $UV_INSTALLER_URL | sh" + elif check_cmd wget; then + say_verbose "Running uv installer with wget" + _install_cmd="wget -qO- $UV_INSTALLER_URL | sh" + else + err "neither curl nor wget is available" + fi + + # run the installer + say_verbose "$_install_cmd" + local _output=$(eval "$_install_cmd" 2>&1) + local _retval=$? + say_verbose "$_output" + if [ $_retval -ne 0 ]; then + err "uv installation failed: $_output" + fi + + # verify uv installation + local _uv_version + _uv_version="$(uv --version 2>/dev/null)" || { + err "could not find uv" + } + + end_activity + if [ -z "$_uv_version" ]; then + err "uv installation failed." + else + say "📦 $_uv_version installed successfully!" + fi +} + +# Install the required Python version +setup_python() { + PYTHON_BIN_PATH="$(uv python find "$PYTHON_VERSION" 2>/dev/null)" || { + PYTHON_BIN_PATH="" + } + if [ -x "$PYTHON_BIN_PATH" ]; then + local _python_version="$($PYTHON_BIN_PATH --version 2>&1)" + say "Python $_python_version is available." + return 0 + else + say "Installing Python $PYTHON_VERSION..." + show_activity + + uv python install "$PYTHON_VERSION" --preview 2>/dev/null || { + err "Failed to install Python" + } + PYTHON_BIN_PATH="$(uv python find "$PYTHON_VERSION")" || { + err "Failed to find Python" + } + + end_activity + fi + + if [ -x "$PYTHON_BIN_PATH" ]; then + local _python_version="$($PYTHON_BIN_PATH --version 2>&1)" + say "🐍 Python $_python_version installed successfully!" + else + err "Failed to install Python" + fi +} + +# Install an official release of the app +install_release() { + say "Installing $APP_NAME..." + show_activity + + local _zip_ext + if check_cmd tar; then + _zip_ext=".tar.gz" + elif check_cmd unzip; then + _zip_ext=".zip" + else + err "could not find tar or unzip" + fi + + local _url="${RELEASE_PATH_URL}/${VERSION}${_zip_ext}" + local _dir="$(ensure mktemp -d)" || return 1 + local _file="$_dir/input$_zip_ext" + local _checksum_file="$_dir/checksum" + + say_verbose "downloading $APP_NAME $VERSION" 1>&2 + say_verbose " from $_url" 1>&2 + say_verbose " to $_file" 1>&2 + + # download tar or zip + if ! download_file "$_url" "$_file"; then + say_verbose "failed to download $_url" + err "Failed to download $APP_NAME $VERSION" + fi + + # download checksum + if ! download_file "$CHECKSUM_URL" "$_checksum_file"; then + say_verbose "failed to download checksum file: $CHECKSUM_URL" + say "Skipping checksum verification" + fi + + # verify checksum + # github action generates checksums in the following format: + # 0.3.4.tar.gz ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb + # 0.3.4.zip 0263829989b6fd954f72baaf2fc64bc2e2f01d692d4de72986ea808f6e99813f + if [ -f $_checksum_file ]; then + # TODO this needs to be tested. + say_verbose "verifying checksum" + local _checksum_value="$(cat "$_checksum_file" | grep "${VERSION}${_zip_ext}" | awk '{print $2}')" + verify_sha256_checksum "$_file" "$_checksum_value" + fi + + # unpack the archive + case "$_zip_ext" in + ".zip") + ensure unzip -q "$_file" -d "$_dir" + ;; + ".tar."*) + ensure tar xf "$_file" --strip-components 1 -C "$_dir" + ;; + *) + err "unknown archive format" + ;; + esac + + # install & cleanup + setup_app "$_dir" + rm -rf "$_dir" + end_activity + say "💥 $APP_NAME $VERSION installed successfully!" +} + +# Install a specific branch/commit/tag from the git repo +install_dev_branch() { + need_cmd git + if [ -z "$DEV_BRANCH" ]; then + err "DEV_BRANCH is not set" + fi + + say "Installing $APP_NAME..." + show_activity + local _dir="$(ensure mktemp -d)" || return 1 + + # clone from git + local _git_url="$REPO_URL.git" + local _git_cmd="git clone --depth 1 $_git_url $_dir" + say_verbose "$_git_cmd" + local _git_out="$($_git_cmd 2>&1)" + say_verbose "$_git_out" + if [ $? -ne 0 ] || echo "$_git_out" | grep -qi "error\|fatal"; then + err "Failed to clone git repo." + fi + + # checkout + local _tag=${DEV_BRANCH#*:} # just the tag name (pull/123/head:pr-123 -> pr-123) + ensure git -C $_dir fetch origin $DEV_BRANCH + ensure git -C $_dir checkout $_tag + + # install & cleanup + setup_app "$_dir" + rm -rf "$_dir" + end_activity + say "🔧 $APP_NAME @ $DEV_BRANCH installed successfully!" +} + +# Install the app in the user's site-packages directory and add a executable +setup_app() { + local _dir="$1" + local _packages_dir="$($PYTHON_BIN_PATH -m site --user-site 2>/dev/null)" || { + err "Failed to find user site packages directory" + } + say_verbose "Installing to $_packages_dir" + local _install_cmd="uv pip install --python="$PYTHON_BIN_PATH" --target="$_packages_dir" --directory="$_dir" ." + say_verbose "$_install_cmd" + local _install_out="$(eval "$_install_cmd" 2>&1)" + say_verbose "$_install_out" + if [ $? -ne 0 ] || echo "$_install_out" | grep -qi "error\|failed\|exception"; then + err "Failed to install $APP_NAME." + fi + + make_python_bin "$HOME/.local/bin/$APP_NAME" + say_verbose "Added bin to ~/.local/bin/$APP_NAME" + + # verify installation + ensure "$APP_NAME" --version > /dev/null +} + +# Initialize a new user project from a template +init_project() { + if [ -z "$INIT_NAME" ]; then + err "INIT_NAME is not set" + fi + if [ -z "$INIT_TEMPLATE" ]; then + INIT_TEMPLATE='empty' + say_verbose "no template specified, defaulting to 'empty'" + fi + + say "Initializing project '$INIT_NAME' from template '$INIT_TEMPLATE'..." + $APP_NAME init "$INIT_NAME" --template "$INIT_TEMPLATE" +} + +update_path_for_shell() { + local _new_path=$1 + local _config_file=$2 + say_verbose "looking for PATH in $_config_file" + if ! grep -E "^[^#]*export[[:space:]]+PATH=.*(:$_new_path|$_new_path:|$_new_path\$)" "$_config_file" >/dev/null 2>&1; then + echo "" >> "$_config_file" # newline + echo "export PATH=\"$_new_path:\$PATH\"" >> "$_config_file" + say_verbose "Added PATH $_new_path to $_config_file" + else + say_verbose "PATH $_new_path already in $_config_file" + fi +} + +# Update PATH in shell config files +update_path() { + local _new_path="$1" + + update_path_for_shell "$_new_path" "$HOME/.bashrc" + update_path_for_shell "$_new_path" "$HOME/.zshrc" + update_path_for_shell "$_new_path" "$HOME/.profile" +} + +# Create a bin file for the app. Assumes entrypoint is main.py:main +make_python_bin() { + local _program_bin="$1" + local _bin_content=$(cat < $_program_bin + chmod +x $_program_bin +} + +uninstall() { + say "Uninstalling $APP_NAME..." + show_activity + + PYTHON_BIN_PATH="$(uv python find "$PYTHON_VERSION" 2>/dev/null)" || { + PYTHON_BIN_PATH="" + } + say_verbose $PYTHON_BIN_PATH + if [ ! -x "$PYTHON_BIN_PATH" ]; then + err "Failed to find Python" + fi + + # uninstall the app + local _packages_dir="$($PYTHON_BIN_PATH -m site --user-site 2>/dev/null)" || { + say_verbose "Failed to find user site packages directory" + } + if [ -d "$_packages_dir" ]; then + say_verbose "Uninstalling from $_packages_dir" + local _uninstall_cmd="uv pip uninstall --python="$PYTHON_BIN_PATH" --target="$_packages_dir" $APP_NAME" + say_verbose "$_uninstall_cmd" + local _uninstall_out="$(eval "$_uninstall_cmd" 2>&1)" + say_verbose "$_uninstall_out" + if [ $? -ne 0 ] || echo "$_uninstall_out" | grep -qi "error\|failed\|exception"; then + err "Failed to uninstall $APP_NAME." + fi + fi + + # remove the bin file + rm -f "$(which $APP_NAME 2>/dev/null)" || { + say_verbose "Failed to find bin file" + } + + end_activity +} + +# uv cache dir can be un-writeable on some systems, perhaps from a previous install +# being executed with `sudo`; use a fallback dir if we need to. +ensure_uv_cache_dir() { + say_verbose "ensuring UV_CACHE_DIR is writeable" + # if cache dir exists, check that it is writeable + if [ ! -e "$CACHE_DIR" ]; then + say_verbose "$CACHE_DIR does not exist; creating" + mkdir -p "$CACHE_DIR" + fi + if [ ! -d "$CACHE_DIR" ] || [ ! -w "$CACHE_DIR" ]; then + say_verbose "Cache directory $CACHE_DIR is not writeable" + say_verbose "Using $HOME/.agentstack-cache instead" + CACHE_DIR="$HOME/.agentstack-cache" + mkdir -p "$CACHE_DIR" + fi + + # if uv cache dir exists, check that it is writeable + UV_CACHE_DIR="$CACHE_DIR/uv" + if [ ! -e "$UV_CACHE_DIR" ]; then + say_verbose "$UV_CACHE_DIR does not exist; creating" + mkdir -p "$UV_CACHE_DIR" 2>&1 || { + say_verbose "Failed to create $UV_CACHE_DIR" + } + fi + if [ ! -d "$UV_CACHE_DIR" ] || [ ! -w "$UV_CACHE_DIR" ]; then + say_verbose "Cache directory $UV_CACHE_DIR is not writeable" + say_verbose "Using $CACHE_DIR/uv-agentstack instead" + UV_CACHE_DIR="$CACHE_DIR/uv-agentstack" + mkdir -p "$UV_CACHE_DIR" + export UV_CACHE_DIR="$UV_CACHE_DIR" + fi +} + +# Download a file. Try curl first, if not installed, use wget instead. +download_file() { + local _url="$1" + local _file="$2" + local _cmd + + if check_cmd curl; then + # use curl + _cmd="curl -sSfL "$_url" -o "$_file"" + elif check_cmd wget; then + # use wget + _cmd="wget -q "$_url" -O "$_file"" + else + err "need curl or wget (command not found)" + return 1 + fi + + local _out + local _out="$($_cmd 2>&1)" || { + say_verbose "$_out" + return 1 + } + return 0 +} + +verify_sha256_checksum() { + local _file="$1" + local _checksum_value="$2" + local _calculated_checksum + + if [ -z "$_checksum_value" ]; then + return 0 + fi + + if ! check_cmd sha256sum; then + say "skipping sha256 checksum verification (requires 'sha256sum' command)" + return 0 + fi + _calculated_checksum="$(sha256sum -b "$_file" | awk '{print $1}')" + + if [ "$_calculated_checksum" != "$_checksum_value" ]; then + err "checksum mismatch + want: $_checksum_value + got: $_calculated_checksum" + fi +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + uninstall) + DO_UNINSTALL=1 + shift + ;; + --version=*) + VERSION="${1#*=}" + shift + ;; + --version) + if [[ -z "$2" || "$2" == -* ]]; then + err "Error: --version requires a value" + usage + exit 1 + fi + VERSION="$2" + shift 2 + ;; + --python-version=*) + PYTHON_VERSION="${1#*=}" + shift + ;; + --python-version) + if [[ -z "$2" || "$2" == -* ]]; then + err "Error: --python-version requires a value" + usage + exit 1 + fi + PYTHON_VERSION="$2" + shift 2 + ;; + --dev-branch=*) + DEV_BRANCH="${1#*=}" + shift + ;; + --dev-branch) + if [[ -z "$2" || "$2" == -* ]]; then + err "Error: --dev-branch requires a value" + usage + exit 1 + fi + DEV_BRANCH="$2" + shift 2 + ;; + --verbose) + PRINT_VERBOSE=1 + shift + ;; + --quiet) + PRINT_QUIET=0 + shift + ;; + -h|--help) + usage + exit 0 + ;; + -*) + err "Unknown option: $1" + usage + exit 1 + ;; + *) + if [[ -z "$COMMAND" ]]; then + COMMAND="$1" + else + err "Unexpected argument: $1" + usage + exit 1 + fi + shift + ;; + esac + done +} + +main() { + parse_args "$@" + + say "$LOGO" + say "" + + # update the path for the current session early + export PATH="$HOME/.local/bin:$PATH" + say_verbose "Session path: $PATH" + + # ensure we have a writeable cache dir for uv + ensure_uv_cache_dir + + if [ $DO_UNINSTALL -eq 1 ]; then + # uninstall requested, uninstall and exit + uninstall + say "" + say "$MSG_UNINSTALL" + say "" + exit 0 + elif check_cmd $APP_NAME; then + # app is already installed, uninstall and proceed to install + say "$MSG_ALREADY_INSTALLED" + uninstall + fi + + say "Starting installation..." + check_dependencies + update_path "$HOME/.local/bin" + install_uv + setup_python + if [ -n "$DEV_BRANCH" ]; then + install_dev_branch + else + install_release + fi + + if [ -n "$INIT_NAME" ]; then + init_project + exit 0 + fi + + say "" + say "$MSG_SUCCESS" + say "" + exit 0 +} + +main "$@" diff --git a/pyproject.toml b/pyproject.toml index 5c9f2aa6..aa045f76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "agentstack" -version = "0.3.5" +version = "0.3.7" description = "The fastest way to build robust AI agents" authors = [ { name="Braelyn Boynton", email="bboynton97@gmail.com" }, @@ -15,7 +15,7 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ - "agentops>=0.3.18", + "agentops>=0.4.9", "typer>=0.12.5", "inquirer>=3.4.0", "art>=6.3", @@ -45,7 +45,7 @@ test = [ "tox", ] crewai = [ - "crewai==0.100.0", + "crewai==0.118.0", "crewai-tools==0.33.0", "shapely==2.0.6" # crewai-tools needs this, but 2.0.7 is broken ] diff --git a/tests/install_script/run_tests.py b/tests/install_script/run_tests.py new file mode 100644 index 00000000..970c290a --- /dev/null +++ b/tests/install_script/run_tests.py @@ -0,0 +1,109 @@ +import os, sys +import io +import re +import hashlib +import tempfile +from pathlib import Path +import docker +from docker.errors import DockerException + +BASE_DIR = Path(__file__).resolve().parent.parent.parent +PYTHON_VERSIONS: list[str] = [">=3.10,<3.13", "3.10", "3.11", "3.12"] + +# make sure your local Docker install has a public socket +# set credstore: "" in ~/.docker/config.json +client = docker.DockerClient(base_url=f'unix://var/run/docker.sock') + + +def print_green(text: str): + print(f"\033[92m{text}\033[0m") + +def print_red(text: str): + print(f"\033[91m{text}\033[0m") + +def _run_vm(name: str, python_version: str, packages: list[str], command: str) -> str: + dockerfile = f""" +FROM ubuntu:latest +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && apt-get install -y {" ".join(packages)} + +WORKDIR /root + +COPY install.sh /root/install.sh +RUN chmod +x /root/install.sh +""" + dockerfile_hash = hashlib.md5(dockerfile.encode("utf-8")).hexdigest() + install_script_hash = hashlib.md5((BASE_DIR / 'install.sh').read_bytes()).hexdigest() + hash = hashlib.md5((dockerfile_hash + install_script_hash).encode("utf-8")).hexdigest() + image_name = F"{re.sub('[<>=,.]', '', python_version)}-{name}-{hash}" + + with tempfile.TemporaryDirectory() as tmpdir: + path = Path(tmpdir) + script = BASE_DIR / 'install.sh' + with open(path / 'install.sh', 'wb') as f: + f.write(script.read_bytes()) + with open(path / 'Dockerfile', 'w') as f: + f.write(dockerfile) + + image, build_logs = client.images.build( + tag=image_name, + path=tmpdir, + rm=True, + ) + + container = client.containers.run( + image=image, + command=command, + detach=False, + ) + return container.decode("utf-8") + + +def test_default(python_version: str): + result = _run_vm( + test_default.__name__, + python_version, + ["build-essential", "git", "curl"], + "bash -c ./install.sh --python-version={python_version}" + ) + assert "Setup complete!" in result + + +def test_wget(python_version: str): + result = _run_vm( + test_wget.__name__, + python_version, + ["build-essential", "git", "wget"], + "bash -c ./install.sh --python-version={python_version}" + ) + assert "Setup complete!" in result + + +def test_dev_branch(python_version: str): + result = _run_vm( + test_dev_branch.__name__, + python_version, + ["build-essential", "git", "curl"], + "bash -c ./install.sh --dev-branch=main --python-version={python_version}" + ) + assert "Setup complete!" in result + + +if __name__ == "__main__": + if "--quick" in sys.argv: + try: + print(f"{PYTHON_VERSIONS[0]}:test_default", end="\t") + test_default(PYTHON_VERSIONS[0]) + print_green(f"PASS") + except AssertionError: + print_red(f"FAIL") + sys.exit(0) + + for method in [func for func in dir() if func.startswith("test_")]: + for version in PYTHON_VERSIONS: + try: + print(f"{version}:{method}", end="\t") + globals()[method](version) + print_green(f"PASS") + except AssertionError: + print_red(f"FAIL") diff --git a/tests/test_cli_init.py b/tests/test_cli_init.py index 1a753455..92ff999d 100644 --- a/tests/test_cli_init.py +++ b/tests/test_cli_init.py @@ -38,7 +38,7 @@ def test_init_command_aliased_framework_empty_project(self, alias: str, framewor if framework != self.framework: self.skipTest(f"{alias} is not related to this framework") + conf.set_path(self.project_dir) # set working dir, init adds `slug_name` init_project(slug_name='test_project', template='empty', framework=alias) - conf.set_path(self.project_dir / 'test_project') config = conf.ConfigFile() assert config.framework == framework