From 5beac59ada307ba0062c1d8f639702cc8f5daafe Mon Sep 17 00:00:00 2001 From: philip ronaldo dang <55852835+philip-shinra@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:27:50 +0530 Subject: [PATCH 1/2] feat: Confluence tool #1334 (#1575) This tool allows to perform following basic operations using atlassian confluence sdk. 1. Get the list of pages in a space . 2. Create a new page in a space. 3. Get a page content by its title and space name. 4. Update a page by using its id. 5. Get space detail. This fixes https://github.com/phidatahq/phidata/issues/1333 --------- Co-authored-by: philip dang Co-authored-by: Dirk Brand <51947788+dirkbrnd@users.noreply.github.com> --- cookbook/tools/confluence_tools.py | 22 ++++ phi/tools/confluence.py | 173 +++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 cookbook/tools/confluence_tools.py create mode 100644 phi/tools/confluence.py diff --git a/cookbook/tools/confluence_tools.py b/cookbook/tools/confluence_tools.py new file mode 100644 index 000000000..8697d52e8 --- /dev/null +++ b/cookbook/tools/confluence_tools.py @@ -0,0 +1,22 @@ +from phi.agent import Agent +from phi.tools.confluence import ConfluenceTools + + +agent = Agent( + name="Confluence agent", + tools=[ConfluenceTools()], + show_tool_calls=True, + markdown=True, +) + +## getting space details +agent.print_response("How many spaces are there and what are their names?") + +## getting page_content +agent.print_response("What is the content present in page 'Large language model in LLM space'") + +## getting page details in a particular space +agent.print_response("Can you extract all the page names from 'LLM space'") + +## creating a new page in a space +agent.print_response("Can you create a new page named 'TESTING' in 'LLM space'") diff --git a/phi/tools/confluence.py b/phi/tools/confluence.py new file mode 100644 index 000000000..f80f85551 --- /dev/null +++ b/phi/tools/confluence.py @@ -0,0 +1,173 @@ +from phi.tools import Toolkit +from phi.utils.log import logger +from typing import Optional +from os import getenv +import json + +try: + from atlassian import Confluence +except (ModuleNotFoundError, ImportError): + raise ImportError("atlassian-python-api not install . Please install using `pip install atlassian-python-api`") + + +class ConfluenceTools(Toolkit): + def __init__( + self, + username: Optional[str] = None, + password: Optional[str] = None, + url: Optional[str] = None, + api_key: Optional[str] = None, + ): + """Initialize Confluence Tools with authentication credentials. + + Args: + username (str, optional): Confluence username. Defaults to None. + password (str, optional): Confluence password. Defaults to None. + url (str, optional): Confluence instance URL. Defaults to None. + api_key (str, optional): Confluence API key. Defaults to None. + + Notes: + Credentials can be provided either through method arguments or environment variables: + - CONFLUENCE_URL + - CONFLUENCE_USERNAME + - CONFLUENCE_API_KEY + """ + + super().__init__(name="confluence_tools") + self.url = url or getenv("CONFLUENCE_URL") + self.username = username or getenv("CONFLUENCE_USERNAME") + self.password = api_key or getenv("CONFLUENCE_API_KEY") or password or getenv("CONFLUENCE_PASSWORD") + + if not self.url: + logger.error( + "Confluence URL not provided. Pass it in the constructor or set CONFLUENCE_URL in environment variable" + ) + + if not self.username: + logger.error( + "Confluence username not provided. Pass it in the constructor or set CONFLUENCE_USERNAME in environment variable" + ) + + if not self.password: + logger.error("Confluence API KEY or password not provided") + + self.confluence = Confluence(url=self.url, username=self.username, password=self.password) + + self.register(self.get_page_content) + self.register(self.get_space_key) + self.register(self.create_page) + self.register(self.update_page) + self.register(self.get_all_space_detail) + self.register(self.get_all_page_from_space) + + def get_page_content(self, space_name: str, page_title: str, expand: Optional[str] = "body.storage"): + """Retrieve the content of a specific page in a Confluence space. + + Args: + space_name (str): Name of the Confluence space. + page_title (str): Title of the page to retrieve. + expand (str, optional): Fields to expand in the page response. Defaults to "body.storage". + + Returns: + str: JSON-encoded page content or error message. + """ + try: + key = self.get_space_key(space_name=space_name) + page = self.confluence.get_page_by_title(key, page_title, expand=expand) + if page: + logger.info(f"Successfully retrieved page '{page_title}' from space '{space_name}'") + return json.dumps(page) + + logger.warning(f"Page '{page_title}' not found in space '{space_name}'") + return json.dumps({"error": f"Page '{page_title}' not found in space '{space_name}'"}) + + except Exception as e: + logger.error(f"Error retrieving page '{page_title}': {e}") + return json.dumps({"error": str(e)}) + + def get_all_space_detail(self): + """Retrieve details about all Confluence spaces. + + Returns: + str: List of space details as a string. + """ + logger.info("Retrieving details for all Confluence spaces") + results = self.confluence.get_all_spaces()["results"] + return str(results) + + def get_space_key(self, space_name: str): + """Get the space key for a particular Confluence space. + + Args: + space_name (str): Name of the space whose key is required. + + Returns: + str: Space key or "No space found" if space doesn't exist. + """ + result = self.confluence.get_all_spaces() + spaces = result["results"] + + for space in spaces: + if space["name"] == space_name: + logger.info(f"Found space key for '{space_name}'") + return space["key"] + + logger.warning(f"No space named {space_name} found") + return "No space found" + + def get_all_page_from_space(self, space_name): + """Retrieve all pages from a specific Confluence space. + + Args: + space_name (str): Name of the Confluence space. + + Returns: + list: Details of pages in the specified space. + """ + logger.info(f"Retrieving all pages from space '{space_name}'") + space_key = self.get_space_key(space_name) + page_details = self.confluence.get_all_pages_from_space( + space_key, status=None, expand=None, content_type="page" + ) + page_details = str([{"id": page["id"], "title": page["title"]} for page in page_details]) + return page_details + + def create_page(self, space_name: str, title: str, body: str, parent_id: Optional[str] = None) -> str: + """Create a new page in Confluence. + + Args: + space_name (str): Name of the Confluence space. + title (str): Title of the new page. + body (str): Content of the new page. + parent_id (str, optional): ID of the parent page if creating a child page. Defaults to None. + + Returns: + str: JSON-encoded page ID and title or error message. + """ + try: + space_key = self.get_space_key(space_name=space_name) + page = self.confluence.create_page(space_key, title, body, parent_id=parent_id) + logger.info(f"Page created: {title} with ID {page['id']}") + return json.dumps({"id": page["id"], "title": title}) + except Exception as e: + logger.error(f"Error creating page '{title}': {e}") + return json.dumps({"error": str(e)}) + + def update_page(self, page_id: str, title: str, body: str) -> str: + """Update an existing Confluence page. + + Args: + page_id (str): ID of the page to update. + title (str): New title for the page. + body (str): Updated content for the page. + + Returns: + str: JSON-encoded status and ID of the updated page or error message. + """ + try: + updated_page = self.confluence.update_page(page_id, title, body) + logger.info(f"Page updated: {title} with ID {updated_page['id']}") + return json.dumps({"status": "success", "id": updated_page["id"]}) + except Exception as e: + logger.error(f"Error updating page '{title}': {e}") + return json.dumps({"error": str(e)}) From e4113a53c3f8cf9ffcf135e868a04f9f28e6f469 Mon Sep 17 00:00:00 2001 From: Dirk Brand <51947788+dirkbrnd@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:41:05 +0200 Subject: [PATCH 2/2] Make confluence cookbook more clear (#1607) ## Description Makes some clarifications on the confluence cookbook to make it more reliable --- cookbook/tools/confluence_tools.py | 4 ++-- phi/tools/confluence.py | 3 ++- phi/tools/eleven_labs_tools.py | 3 +-- pyproject.toml | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cookbook/tools/confluence_tools.py b/cookbook/tools/confluence_tools.py index 8697d52e8..6e989909d 100644 --- a/cookbook/tools/confluence_tools.py +++ b/cookbook/tools/confluence_tools.py @@ -16,7 +16,7 @@ agent.print_response("What is the content present in page 'Large language model in LLM space'") ## getting page details in a particular space -agent.print_response("Can you extract all the page names from 'LLM space'") +agent.print_response("Can you extract all the page names from 'LLM' space") ## creating a new page in a space -agent.print_response("Can you create a new page named 'TESTING' in 'LLM space'") +agent.print_response("Can you create a new page named 'TESTING' in 'LLM' space") diff --git a/phi/tools/confluence.py b/phi/tools/confluence.py index f80f85551..e318a34cc 100644 --- a/phi/tools/confluence.py +++ b/phi/tools/confluence.py @@ -72,6 +72,7 @@ def get_page_content(self, space_name: str, page_title: str, expand: Optional[st str: JSON-encoded page content or error message. """ try: + logger.info(f"Retrieving page content from space '{space_name}'") key = self.get_space_key(space_name=space_name) page = self.confluence.get_page_by_title(key, page_title, expand=expand) if page: @@ -115,7 +116,7 @@ def get_space_key(self, space_name: str): logger.warning(f"No space named {space_name} found") return "No space found" - def get_all_page_from_space(self, space_name): + def get_all_page_from_space(self, space_name: str): """Retrieve all pages from a specific Confluence space. Args: diff --git a/phi/tools/eleven_labs_tools.py b/phi/tools/eleven_labs_tools.py index 7082b0264..8b307733f 100644 --- a/phi/tools/eleven_labs_tools.py +++ b/phi/tools/eleven_labs_tools.py @@ -130,8 +130,7 @@ def generate_sound_effect(self, agent: Agent, prompt: str, duration_seconds: Opt """ try: audio_generator = self.eleven_labs_client.text_to_sound_effects.convert( - text=prompt, - duration_seconds=duration_seconds + text=prompt, duration_seconds=duration_seconds ) base64_audio = self._process_audio(audio_generator) diff --git a/pyproject.toml b/pyproject.toml index dbefd4499..1ca8e856e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,6 +98,7 @@ module = [ "anthropic.*", "apify_client.*", "arxiv.*", + "atlassian.*", "boto3.*", "botocore.*", "bs4.*",