diff --git a/cookbook/claude/README.md b/cookbook/claude/README.md index 3c9d39cdb3..bc79d11b75 100644 --- a/cookbook/claude/README.md +++ b/cookbook/claude/README.md @@ -20,35 +20,51 @@ export ANTHROPIC_API_KEY=xxx ### 3. Install libraries ```shell -pip install -U anthropic phidata duckduckgo-search duckdb yfinance exa_py +pip install -U anthropic duckduckgo-search duckdb yfinance exa_py phidata ``` -### 4. Web search function calling +### 4. Run Assistant + +- stream on ```shell -python cookbook/claude/web_search.py +python cookbook/claude/assistant.py ``` -### 5. YFinance function calling +- stream off ```shell -python cookbook/claude/finance.py +python cookbook/claude/assistant_stream_off.py ``` -### 6. Structured output +### 5. Run Assistant with Tools + +- Web search ```shell -python cookbook/claude/structured_output.py +python cookbook/claude/web_search.py +``` + +- YFinance + +```shell +python cookbook/claude/finance.py ``` -### 7. Data Analyst +- Data Analyst ```shell python cookbook/claude/data_analyst.py ``` -### 8. Exa Search +- Exa Search ```shell python cookbook/claude/exa_search.py ``` + +### 6. Run Assistant with Structured output + +```shell +python cookbook/claude/structured_output.py +``` diff --git a/cookbook/claude/exa_search.py b/cookbook/claude/exa_search.py index 07d0193524..a55655cd29 100644 --- a/cookbook/claude/exa_search.py +++ b/cookbook/claude/exa_search.py @@ -1,6 +1,12 @@ from phi.assistant import Assistant -from phi.llm.anthropic import Claude from phi.tools.exa import ExaTools +from phi.tools.website import WebsiteTools +from phi.llm.anthropic import Claude -assistant = Assistant(llm=Claude(), tools=[ExaTools()], show_tool_calls=True) -assistant.cli_app(markdown=True) +assistant = Assistant(llm=Claude(), tools=[ExaTools(), WebsiteTools()], show_tool_calls=True) +assistant.print_response( + "Produce this table: research chromatic homotopy theory." + "Access each link in the result outputting the summary for that article, its link, and keywords; " + "After the table output make conceptual ascii art of the overarching themes and constructions", + markdown=True, +) diff --git a/cookbook/claude/finance.py b/cookbook/claude/finance.py index 9d0fe0013d..196b09d473 100644 --- a/cookbook/claude/finance.py +++ b/cookbook/claude/finance.py @@ -3,9 +3,13 @@ from phi.llm.anthropic import Claude assistant = Assistant( - llm=Claude(model="claude-3-opus-20240229"), + name="Finance Assistant", + llm=Claude(model="claude-3-haiku-20240307"), tools=[YFinanceTools(stock_price=True, analyst_recommendations=True, stock_fundamentals=True)], show_tool_calls=True, + description="You are an investment analyst that researches stock prices, analyst recommendations, and stock fundamentals.", + instructions=["Format your response using markdown and use tables to display data where possible."], + # debug_mode=True, ) -assistant.print_response("Share the NVDA stock price and some analyst recommendations", markdown=True) -assistant.print_response("Summarize fundamentals for TSLA", markdown=True) +assistant.print_response("Share the NVDA stock price and analyst recommendations", markdown=True) +# assistant.print_response("Summarize fundamentals for TSLA", markdown=True) diff --git a/cookbook/claude/web_search.py b/cookbook/claude/web_search.py index 250b1695a7..f5799f182c 100644 --- a/cookbook/claude/web_search.py +++ b/cookbook/claude/web_search.py @@ -6,6 +6,6 @@ llm=Claude(model="claude-3-opus-20240229"), tools=[DuckDuckGo()], show_tool_calls=True, - debug_mode=True, + # debug_mode=True, ) assistant.print_response("Share 1 story from france and 1 from germany?", markdown=True, stream=False) diff --git a/cookbook/cohere/README.md b/cookbook/cohere/README.md new file mode 100644 index 0000000000..29892422f9 --- /dev/null +++ b/cookbook/cohere/README.md @@ -0,0 +1,48 @@ +# CohereChat function calling + +Currently "command-r" model supports function calling + +> Note: Fork and clone this repository if needed + +### 1. Create and activate a virtual environment + +```shell +python3 -m venv ~/.venvs/aienv +source ~/.venvs/aienv/bin/activate +``` + +### 2. Export your CohereChat API Key + +```shell +export CO_API_KEY=xxx +``` + +### 3. Install libraries + +```shell +pip install -U cohere duckduckgo-search yfinance exa_py phidata +``` + +### 4. Web search function calling + +```shell +python cookbook/cohere/web_search.py +``` + +### 5. YFinance function calling + +```shell +python cookbook/cohere/finance.py +``` + +### 6. Structured output + +```shell +python cookbook/cohere/structured_output.py +``` + +### 7. Exa Search + +```shell +python cookbook/cohere/exa_search.py +``` diff --git a/cookbook/cohere/__init__.py b/cookbook/cohere/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cookbook/cohere/assistant.py b/cookbook/cohere/assistant.py new file mode 100644 index 0000000000..177a8ace4f --- /dev/null +++ b/cookbook/cohere/assistant.py @@ -0,0 +1,9 @@ +from phi.assistant import Assistant +from phi.llm.cohere import CohereChat + +assistant = Assistant( + llm=CohereChat(model="command-r"), + description="You help people with their health and fitness goals.", + debug_mode=True, +) +assistant.print_response("Share a quick healthy breakfast recipe.", markdown=True) diff --git a/cookbook/cohere/assistant_stream_off.py b/cookbook/cohere/assistant_stream_off.py new file mode 100644 index 0000000000..b5ee1e9ef9 --- /dev/null +++ b/cookbook/cohere/assistant_stream_off.py @@ -0,0 +1,9 @@ +from phi.assistant import Assistant +from phi.llm.cohere import CohereChat + +assistant = Assistant( + llm=CohereChat(model="command-r"), + description="You help people with their health and fitness goals.", + debug_mode=True, +) +assistant.print_response("Share a quick healthy breakfast recipe.", markdown=True, stream=False) diff --git a/cookbook/cohere/data_analyst.py b/cookbook/cohere/data_analyst.py new file mode 100644 index 0000000000..75c50edc44 --- /dev/null +++ b/cookbook/cohere/data_analyst.py @@ -0,0 +1,20 @@ +from phi.assistant import Assistant +from phi.llm.cohere import CohereChat +from phi.tools.duckdb import DuckDbTools + +duckdb_tools = DuckDbTools(create_tables=False, export_tables=False, summarize_tables=False) +duckdb_tools.create_table_from_path( + path="https://phidata-public.s3.amazonaws.com/demo_data/IMDB-Movie-Data.csv", table="movies" +) + +assistant = Assistant( + llm=CohereChat(model="command-r-plus"), + tools=[duckdb_tools], + show_tool_calls=True, + add_to_system_prompt=""" + Here are the tables you have access to: + - movies: Contains information about movies from IMDB. + """, + # debug_mode=True, +) +assistant.print_response("What is the average rating of movies?", markdown=True, stream=False) diff --git a/cookbook/cohere/exa_search.py b/cookbook/cohere/exa_search.py new file mode 100644 index 0000000000..09e15b29d2 --- /dev/null +++ b/cookbook/cohere/exa_search.py @@ -0,0 +1,12 @@ +from phi.assistant import Assistant +from phi.tools.exa import ExaTools +from phi.tools.website import WebsiteTools +from phi.llm.cohere import CohereChat + +assistant = Assistant(llm=CohereChat(model="command-r-plus"), tools=[ExaTools(), WebsiteTools()], show_tool_calls=True) +assistant.print_response( + "Produce this table: research chromatic homotopy theory." + "Access each link in the result outputting the summary for that article, its link, and keywords; " + "After the table output make conceptual ascii art of the overarching themes and constructions", + markdown=True, +) diff --git a/cookbook/cohere/finance.py b/cookbook/cohere/finance.py new file mode 100644 index 0000000000..59d456a4d5 --- /dev/null +++ b/cookbook/cohere/finance.py @@ -0,0 +1,14 @@ +from phi.assistant import Assistant +from phi.tools.yfinance import YFinanceTools +from phi.llm.cohere import CohereChat + +assistant = Assistant( + llm=CohereChat(model="command-r-plus"), + tools=[YFinanceTools(stock_price=True, analyst_recommendations=True, stock_fundamentals=True)], + show_tool_calls=True, + description="You are an investment analyst that researches stock prices, analyst recommendations, and stock fundamentals.", + instructions=["Format your response using markdown and use tables to display data where possible."], + # debug_mode=True, +) +assistant.print_response("Share the NVDA stock price and analyst recommendations", markdown=True) +assistant.print_response("Summarize fundamentals for TSLA", markdown=True) diff --git a/cookbook/cohere/structured_output.py b/cookbook/cohere/structured_output.py new file mode 100644 index 0000000000..678a573fef --- /dev/null +++ b/cookbook/cohere/structured_output.py @@ -0,0 +1,26 @@ +from typing import List +from pydantic import BaseModel, Field +from rich.pretty import pprint +from phi.assistant import Assistant +from phi.llm.cohere import CohereChat + + +class MovieScript(BaseModel): + setting: str = Field(..., description="Provide a nice setting for a blockbuster movie.") + ending: str = Field(..., description="Ending of the movie. If not available, provide a happy ending.") + genre: str = Field( + ..., description="Genre of the movie. If not available, select action, thriller or romantic comedy." + ) + name: str = Field(..., description="Give a name to this movie") + characters: List[str] = Field(..., description="Name of characters for this movie.") + storyline: str = Field(..., description="3 sentence storyline for the movie. Make it exciting!") + + +movie_assistant = Assistant( + llm=CohereChat(model="command-r"), + description="You help people write movie scripts.", + output_model=MovieScript, + debug_mode=True, +) + +pprint(movie_assistant.run("New York")) diff --git a/cookbook/cohere/web_search.py b/cookbook/cohere/web_search.py new file mode 100644 index 0000000000..d085411390 --- /dev/null +++ b/cookbook/cohere/web_search.py @@ -0,0 +1,12 @@ +from phi.assistant import Assistant +from phi.tools.duckduckgo import DuckDuckGo +from phi.llm.cohere import CohereChat + +assistant = Assistant( + llm=CohereChat(model="command-r"), + tools=[DuckDuckGo()], + show_tool_calls=True, + instructions=["Format your response using markdown and use tables to display information where possible."], + debug_mode=True, +) +assistant.print_response("Share 1 story from france and 1 from germany?", markdown=True) diff --git a/cookbook/groq/finance.py b/cookbook/groq/finance.py new file mode 100644 index 0000000000..ce0bbe5e38 --- /dev/null +++ b/cookbook/groq/finance.py @@ -0,0 +1,12 @@ +from phi.assistant import Assistant +from phi.tools.yfinance import YFinanceTools +from phi.llm.groq import Groq + +assistant = Assistant( + llm=Groq(model="mixtral-8x7b-32768"), + tools=[YFinanceTools(stock_price=True, analyst_recommendations=True, stock_fundamentals=True)], + show_tool_calls=True, + # debug_mode=True, +) +assistant.print_response("Share the NVDA stock price and analyst recommendations", markdown=True, stream=False) +# assistant.print_response("Summarize fundamentals for TSLA", markdown=True, stream=False) diff --git a/cookbook/groq/structured_output.py b/cookbook/groq/structured_output.py new file mode 100644 index 0000000000..43f9aa1096 --- /dev/null +++ b/cookbook/groq/structured_output.py @@ -0,0 +1,26 @@ +from typing import List +from pydantic import BaseModel, Field +from rich.pretty import pprint +from phi.assistant import Assistant +from phi.llm.groq import Groq + + +class MovieScript(BaseModel): + setting: str = Field(..., description="Provide a nice setting for a blockbuster movie.") + ending: str = Field(..., description="Ending of the movie. If not available, provide a happy ending.") + genre: str = Field( + ..., description="Genre of the movie. If not available, select action, thriller or romantic comedy." + ) + name: str = Field(..., description="Give a name to this movie") + characters: List[str] = Field(..., description="Name of characters for this movie.") + storyline: str = Field(..., description="3 sentence storyline for the movie. Make it exciting!") + + +movie_assistant = Assistant( + llm=Groq(model="mixtral-8x7b-32768"), + description="You help people write movie scripts.", + output_model=MovieScript, + # debug_mode=True, +) + +pprint(movie_assistant.run("New York")) diff --git a/cookbook/groq/web_search.py b/cookbook/groq/web_search.py new file mode 100644 index 0000000000..f3d748e773 --- /dev/null +++ b/cookbook/groq/web_search.py @@ -0,0 +1,11 @@ +from phi.assistant import Assistant +from phi.tools.duckduckgo import DuckDuckGo +from phi.llm.groq import Groq + +assistant = Assistant( + llm=Groq(model="mixtral-8x7b-32768"), + tools=[DuckDuckGo()], + show_tool_calls=True, + # debug_mode=True +) +assistant.print_response("Tell me about OpenAI Sora", markdown=True, stream=False) diff --git a/cookbook/hermes2/assistant.py b/cookbook/hermes2/assistant.py index 4ccfcb2494..70d677a938 100644 --- a/cookbook/hermes2/assistant.py +++ b/cookbook/hermes2/assistant.py @@ -1,8 +1,8 @@ from phi.assistant import Assistant -from phi.llm.ollama import Ollama +from phi.llm.ollama import Hermes assistant = Assistant( - llm=Ollama(model="adrienbrault/nous-hermes2pro:Q8_0"), + llm=Hermes(model="adrienbrault/nous-hermes2pro:Q8_0"), description="You help people with their health and fitness goals.", ) assistant.print_response("Share a quick healthy breakfast recipe.", markdown=True) diff --git a/cookbook/hermes2/finance.py b/cookbook/hermes2/finance.py index 4fd32620db..e86f7ead21 100644 --- a/cookbook/hermes2/finance.py +++ b/cookbook/hermes2/finance.py @@ -7,5 +7,5 @@ tools=[YFinanceTools(stock_price=True, analyst_recommendations=True, stock_fundamentals=True)], show_tool_calls=True, ) -assistant.print_response("Share the NVDA stock price and some analyst recommendations", markdown=True) +assistant.print_response("Share the NVDA stock price and analyst recommendations", markdown=True) assistant.print_response("Summarize fundamentals for TSLA", markdown=True) diff --git a/cookbook/knowledge/json.py b/cookbook/knowledge/json.py index 11b2b903fe..5343b3adc3 100644 --- a/cookbook/knowledge/json.py +++ b/cookbook/knowledge/json.py @@ -26,4 +26,4 @@ ) # Use the assistant -assistant.print_response("Ask me about something from the knowledge base", markdown=True) \ No newline at end of file +assistant.print_response("Ask me about something from the knowledge base", markdown=True) diff --git a/cookbook/pinecone/assistant.py b/cookbook/pinecone/assistant.py index 6c31add52b..b5c99ce9ab 100644 --- a/cookbook/pinecone/assistant.py +++ b/cookbook/pinecone/assistant.py @@ -1,10 +1,11 @@ +import os import typer +from typing import Optional from rich.prompt import Prompt -from typing import Optional, List + from phi.assistant import Assistant from phi.knowledge.pdf import PDFUrlKnowledgeBase from phi.vectordb.pineconedb import PineconeDB -import os api_key = os.getenv("PINECONE_API_KEY") index_name = "recipes" @@ -28,6 +29,7 @@ # Comment out after first run # knowledge_base.load(recreate=False) + def pdf_assistant(user: str = "user"): run_id: Optional[str] = None @@ -55,5 +57,6 @@ def pdf_assistant(user: str = "user"): break assistant.print_response(message) + if __name__ == "__main__": typer.run(pdf_assistant) diff --git a/cookbook/teams/hackernews.py b/cookbook/teams/hackernews.py index 59f53b7be4..00cb876fe4 100644 --- a/cookbook/teams/hackernews.py +++ b/cookbook/teams/hackernews.py @@ -1,7 +1,7 @@ import json import httpx -from phi.assistant.team import Assistant, Team +from phi.assistant.team import Assistant from phi.utils.log import logger @@ -95,5 +95,5 @@ def get_user_details(username: str) -> str: show_tool_calls=True, ) -team = Team(name="HackerNews", assistants=[hn_top_stories, hn_user_researcher], debug_mode=True) -team.print_response("Tell me about the users with the top 2 stores on hackernews?", markdown=True) +hn_assistant = Assistant(name="HackerNews Assistant", team=[hn_top_stories, hn_user_researcher], debug_mode=True) +hn_assistant.print_response("Tell me about the users with the top 2 stores on hackernews?", markdown=True) diff --git a/cookbook/teams/investment.py b/cookbook/teams/investment.py new file mode 100644 index 0000000000..f5721f4326 --- /dev/null +++ b/cookbook/teams/investment.py @@ -0,0 +1,66 @@ +from pathlib import Path +from shutil import rmtree +from phi.assistant.team import Assistant +from phi.tools.yfinance import YFinanceTools +from phi.tools.file import FileTools + + +reports_dir = Path(__file__).parent.parent.parent.joinpath("junk", "reports") +if reports_dir.exists(): + rmtree(path=reports_dir, ignore_errors=True) +reports_dir.mkdir(parents=True, exist_ok=True) + +stock_analyst = Assistant( + name="Stock Analyst", + role="Get current stock price, analyst recommendations and news for a company.", + tools=[ + YFinanceTools(stock_price=True, analyst_recommendations=True, company_news=True), + FileTools(base_dir=reports_dir), + ], + description="You are an stock analyst tasked with producing factual reports on companies.", + instructions=[ + "The investment lead will provide you with a list of companies to write reports on.", + "Get the current stock price, analyst recommendations and news for the company", + "Save it to a file in markdown format with the the name `company_name.md`.", + "Let the investment lead know the file name of the report.", + ], + debug_mode=True, +) +research_analyst = Assistant( + name="Research Analyst", + role="Writes research reports on stocks.", + tools=[FileTools(base_dir=reports_dir)], + description="You are an investment researcher analyst tasked with producing a ranked list of companies based on their investment potential.", + instructions=[ + "You will write your research report based on the information available in files produced by the stock analyst.", + "The investment lead will provide you with the files saved by the stock analyst." + "If no files are provided, list all files in the entire folder and read the files with names matching company names.", + "Read each file 1 by 1.", + "Then think deeply about whether a stock is valuable or not. Be discerning, you are a skeptical investor.", + "Finally, save your research report to a file called `research_report.md`.", + ], + debug_mode=True, +) + +investment_lead = Assistant( + name="Investment Lead", + team=[stock_analyst, research_analyst], + tools=[FileTools(base_dir=reports_dir)], + description="You are an investment lead tasked with producing a research report on companies for investment purposes.", + instructions=[ + "Given a list of companies, first ask the stock analyst to get the current stock price, analyst recommendations and news for these companies.", + "Ask the stock analyst to write its results to files in markdown format with the the name `company_name.md`.", + "If the stock analyst has not saved the file or saved it with an incorrect name, ask them to save the file again before proceeding." + "Then ask the research_analyst to write a report on these companies based on the information provided by the stock analyst.", + "Make sure to provide the research analyst with the files saved by the stock analyst and ask it to read the files directly." + "The research analyst should save its report to a file called `research_report.md`.", + "Finally, review the research report and answer the users question. Make sure to answer their question correctly, in a clear and concise manner.", + "If the research analyst has not completed the report, ask them to complete it before you can answer the users question.", + "Produce a nicely formatted response to the user, use markdown to format the response.", + ], + debug_mode=True, +) +investment_lead.print_response( + "How would you invest $10000 in META, GOOG, NVDA and TSLA? Tell me the exact amount you'd invest in each.", + markdown=True, +) diff --git a/cookbook/teams/journalist.py b/cookbook/teams/journalist.py new file mode 100644 index 0000000000..6a61604984 --- /dev/null +++ b/cookbook/teams/journalist.py @@ -0,0 +1,52 @@ +""" +Inspired by the fantastic work by Matt Shumer (@mattshumer_): https://twitter.com/mattshumer_/status/1772286375817011259 + +Please run: +pip install openai anthropic google-search-results newspaper3k lxml_html_clean phidata +""" + +from phi.assistant.team import Assistant +from phi.tools.serpapi_toolkit import SerpApiToolkit +from phi.tools.newspaper_toolkit import NewspaperToolkit +from phi.llm.anthropic import Claude + + +search_journalist = Assistant( + name="Search Journalist", + llm=Claude(model="claude-3-haiku-20240307"), + role="Searches for top URLs based on a topic", + description="You are a world-class search journalist. You search the web and retrieve the top URLs based on a topic.", + instructions=[ + "Given a topic, conduct a search to find the top results.", + "For a search topic, return the top 3 URLs.", + ], + tools=[SerpApiToolkit()], + debug_mode=True, +) +research_journalist = Assistant( + name="Research Journalist", + llm=Claude(model="claude-3-haiku-20240307"), + role="Retrieves text from URLs", + description="You are a world-class research journalist. You retrieve the text from the URLs.", + instructions=["For a list of URLs, return the text of the articles."], + tools=[NewspaperToolkit()], + debug_mode=True, +) + + +editor = Assistant( + name="Editor", + team=[search_journalist, research_journalist], + description="You are a senior NYT editor. Given a topic, use the journalists to write a NYT worthy article.", + instructions=[ + "Given a topic, ask the search journalist to search for the top 3 URLs.", + "Then pass on these URLs to research journalist to get the text of the articles.", + "Use the text of the articles to write an article about the topic.", + "Make sure to write a well researched article with a clear and concise message.", + "The article should be extremely articulate and well written. " + "Focus on clarity, coherence, and overall quality.", + ], + debug_mode=True, + markdown=True, +) +editor.print_response("Write an article about latest developments in AI.") diff --git a/phi/assistant/assistant.py b/phi/assistant/assistant.py index f6e9ca13c8..f3868ca6a2 100644 --- a/phi/assistant/assistant.py +++ b/phi/assistant/assistant.py @@ -1,4 +1,6 @@ import json +from os import getenv +from textwrap import dedent from uuid import uuid4 from typing import List, Any, Optional, Dict, Iterator, Callable, Union, Type, Tuple, Literal @@ -191,13 +193,17 @@ class Assistant(BaseModel): # Metadata associated with the assistant tasks task_data: Optional[Dict[str, Any]] = None - # -*- Team settings + # -*- Assistant Team + team: Optional[List["Assistant"]] = None + # When the assistant is part of a team, this is the role of the assistant in the team role: Optional[str] = None + # Add instructions for delegating tasks to another assistant + add_delegation_instructions: bool = True # debug_mode=True enables debug logs debug_mode: bool = False # monitoring=True logs Assistant runs on phidata.com - monitoring: bool = False + monitoring: bool = getenv("PHI_MONITORING", "false").lower() == "true" model_config = ConfigDict(arbitrary_types_allowed=True) @@ -220,6 +226,13 @@ def streamable(self) -> bool: def llm_task(self) -> LLMTask: """Returns an LLMTask for this assistant""" + tools = self.tools + if self.team and len(self.team) > 0: + if tools is None: + tools = [] + for assistant_index, assistant in enumerate(self.team): + tools.append(self.get_delegation_function(assistant, assistant_index)) + _llm_task = LLMTask( llm=self.llm.model_copy() if self.llm is not None else None, assistant_name=self.name, @@ -231,7 +244,7 @@ def llm_task(self) -> LLMTask: use_tools=self.use_tools, show_tool_calls=self.show_tool_calls, tool_call_limit=self.tool_call_limit, - tools=self.tools, + tools=tools, tool_choice=self.tool_choice, read_chat_history_tool=self.read_chat_history_tool, search_knowledge_base_tool=self.search_knowledge_base_tool, @@ -251,6 +264,7 @@ def llm_task(self) -> LLMTask: prevent_prompt_injection=self.prevent_prompt_injection, limit_tool_access=self.limit_tool_access, add_datetime_to_instructions=self.add_datetime_to_instructions, + add_delegation_instructions=self.add_delegation_instructions, markdown=self.markdown, user_prompt=self.user_prompt, user_prompt_template=self.user_prompt_template, @@ -260,9 +274,51 @@ def llm_task(self) -> LLMTask: references_format=self.references_format, chat_history_function=self.chat_history_function, output_model=self.output_model, + delegation_prompt=self.get_delegation_prompt(), ) return _llm_task + def get_delegation_function(self, assistant: "Assistant", index: int) -> Function: + def _delegate_task_to_assistant(task_description: str) -> str: + return assistant.run(task_description, stream=False) # type: ignore + + assistant_name = assistant.name.replace(" ", "_").lower() if assistant.name else f"assistant_{index}" + delegation_function = Function.from_callable(_delegate_task_to_assistant) + delegation_function.name = f"delegate_task_to_{assistant_name}" + delegation_function.description = dedent( + f"""Use this function to delegate a task to {assistant_name} + Args: + task_description (str): A clear and concise description of the task the assistant should achieve. + Returns: + str: The result of the delegated task. + """ + ) + return delegation_function + + def get_delegation_prompt(self) -> Optional[str]: + if self.team and len(self.team) > 0: + delegation_prompt = "You can delegate tasks to the following assistants:" + delegation_prompt += "\n" + for assistant_index, assistant in enumerate(self.team): + delegation_prompt += f"\nAssistant {assistant_index + 1}:\n" + if assistant.name: + delegation_prompt += f"Name: {assistant.name}\n" + if assistant.role: + delegation_prompt += f"Role: {assistant.role}\n" + if assistant.tools is not None: + _tools = [] + for _tool in assistant.tools: + if isinstance(_tool, Toolkit): + _tools.extend(list(_tool.functions.keys())) + elif isinstance(_tool, Function): + _tools.append(_tool.name) + elif callable(_tool): + _tools.append(_tool.__name__) + delegation_prompt += f"Available tools: {', '.join(_tools)}\n" + delegation_prompt += "" + return delegation_prompt + return None + def to_database_row(self) -> AssistantRun: """Create a AssistantRun for the current Assistant (to save to the database)""" @@ -524,11 +580,11 @@ def _run( # -*- Update run output self.output = run_output + logger.debug(f"*********** Run End: {self.run_id} ***********") # -*- Yield final response if not streaming if not stream: yield run_output - logger.debug(f"*********** Run End: {self.run_id} ***********") def run( self, message: Optional[Union[List, Dict, str]] = None, stream: bool = True, **kwargs: Any diff --git a/phi/assistant/team/team.py b/phi/assistant/team/team.py index 609fa716dd..d32c1a3419 100644 --- a/phi/assistant/team/team.py +++ b/phi/assistant/team/team.py @@ -85,8 +85,8 @@ def leader(self) -> Assistant: else: _system_prompt += "You are an AI Assistant" - _system_prompt += "and your goal is to respond to the users message in the best way possible. " - _system_prompt += "This is an important task and must be done with correctly.\n\n" + _system_prompt += " and your goal is to respond to the users message in the best way possible. " + _system_prompt += "This is an important task and must be done correctly.\n\n" if self.assistants and len(self.assistants) > 0: _system_prompt += ( @@ -110,7 +110,9 @@ def leader(self) -> Assistant: _system_prompt += "\n" if self.reviewer is None: - _system_prompt += "You must always review the responses from the assistants and re-run tasks if the result is not satisfactory." + _system_prompt += ( + "You must review the responses from the assistants and re-run tasks if the result is not satisfactory." + ) return Assistant( system_prompt=_system_prompt, diff --git a/phi/file/local/txt.py b/phi/file/local/txt.py index 6c2e6ce5df..795c9fe7e9 100644 --- a/phi/file/local/txt.py +++ b/phi/file/local/txt.py @@ -2,6 +2,7 @@ from phi.file import File + class TextFile(File): path: str type: str = "TEXT" diff --git a/phi/llm/base.py b/phi/llm/base.py index 389d819ddd..91ed87f33f 100644 --- a/phi/llm/base.py +++ b/phi/llm/base.py @@ -46,6 +46,9 @@ class LLM(BaseModel): system_prompt: Optional[str] = None instructions: Optional[List[str]] = None + # State from the run + run_id: Optional[str] = None + model_config = ConfigDict(arbitrary_types_allowed=True) @property diff --git a/phi/llm/cohere/__init__.py b/phi/llm/cohere/__init__.py new file mode 100644 index 0000000000..b3b0e328d6 --- /dev/null +++ b/phi/llm/cohere/__init__.py @@ -0,0 +1 @@ +from phi.llm.cohere.chat import CohereChat diff --git a/phi/llm/cohere/chat.py b/phi/llm/cohere/chat.py new file mode 100644 index 0000000000..39e9cc8b82 --- /dev/null +++ b/phi/llm/cohere/chat.py @@ -0,0 +1,393 @@ +import json +from textwrap import dedent +from typing import Optional, List, Dict, Any, Iterator + +from phi.llm.base import LLM +from phi.llm.message import Message +from phi.tools.function import FunctionCall +from phi.utils.log import logger +from phi.utils.timer import Timer +from phi.utils.tools import get_function_call_for_tool_call + +try: + from cohere import Client as CohereClient + from cohere.types.tool import Tool as CohereTool + from cohere.types.tool_call import ToolCall as CohereToolCall + from cohere.types.non_streamed_chat_response import NonStreamedChatResponse + from cohere.types.streamed_chat_response import ( + StreamedChatResponse, + StreamedChatResponse_StreamStart, + StreamedChatResponse_TextGeneration, + StreamedChatResponse_ToolCallsGeneration, + ) + from cohere.types.chat_request_tool_results_item import ChatRequestToolResultsItem + from cohere.types.tool_parameter_definitions_value import ToolParameterDefinitionsValue +except ImportError: + logger.error("`cohere` not installed") + raise + + +class CohereChat(LLM): + name: str = "cohere" + model: str = "command-r" + # -*- Request parameters + temperature: Optional[float] = None + max_tokens: Optional[int] = None + top_k: Optional[int] = None + top_p: Optional[float] = None + frequency_penalty: Optional[float] = None + presence_penalty: Optional[float] = None + request_params: Optional[Dict[str, Any]] = None + # Add chat history to the cohere messages instead of using the conversation_id + add_chat_history: bool = False + # -*- Client parameters + api_key: Optional[str] = None + client_params: Optional[Dict[str, Any]] = None + # -*- Provide the Cohere client manually + cohere_client: Optional[CohereClient] = None + + @property + def client(self) -> CohereClient: + if self.cohere_client: + return self.cohere_client + + _client_params: Dict[str, Any] = {} + if self.api_key: + _client_params["api_key"] = self.api_key + return CohereClient(**_client_params) + + @property + def api_kwargs(self) -> Dict[str, Any]: + _request_params: Dict[str, Any] = {} + if self.run_id is not None: + _request_params["conversation_id"] = self.run_id + if self.temperature: + _request_params["temperature"] = self.temperature + if self.max_tokens: + _request_params["max_tokens"] = self.max_tokens + if self.top_k: + _request_params["top_k"] = self.top_k + if self.top_p: + _request_params["top_p"] = self.top_p + if self.frequency_penalty: + _request_params["frequency_penalty"] = self.frequency_penalty + if self.presence_penalty: + _request_params["presence_penalty"] = self.presence_penalty + if self.request_params: + _request_params.update(self.request_params) + return _request_params + + def get_tools(self) -> Optional[List[CohereTool]]: + if not self.functions: + return None + + # Returns the tools in the format required by the Cohere API + return [ + CohereTool( + name=f_name, + description=function.description or "", + parameter_definitions={ + param_name: ToolParameterDefinitionsValue( + type=param_info["type"] if isinstance(param_info["type"], str) else param_info["type"][0], + required="null" not in param_info["type"], + ) + for param_name, param_info in function.parameters.get("properties", {}).items() + }, + ) + for f_name, function in self.functions.items() + ] + + def invoke( + self, messages: List[Message], tool_results: Optional[List[ChatRequestToolResultsItem]] = None + ) -> NonStreamedChatResponse: + api_kwargs: Dict[str, Any] = self.api_kwargs + chat_message: Optional[str] = None + + if self.add_chat_history: + logger.debug("Providing chat_history to cohere") + chat_history = [] + for m in messages: + if m.role == "system" and "preamble" not in api_kwargs: + api_kwargs["preamble"] = m.content + elif m.role == "user": + if chat_message is not None: + # Add the existing chat_message to the chat_history + chat_history.append({"role": "USER", "message": chat_message}) + # Update the chat_message to the new user message + chat_message = m.get_content_string() + else: + chat_history.append({"role": "CHATBOT", "message": m.get_content_string() or ""}) + api_kwargs["chat_history"] = chat_history + else: + # Set first system message as preamble + for m in messages: + if m.role == "system" and "preamble" not in api_kwargs: + api_kwargs["preamble"] = m.get_content_string() + break + # Set last user message as chat_message + for m in reversed(messages): + if m.role == "user": + chat_message = m.get_content_string() + break + + if self.tools: + api_kwargs["tools"] = self.get_tools() + + if tool_results: + api_kwargs["tool_results"] = tool_results + + return self.client.chat(message=chat_message or "", model=self.model, **api_kwargs) + + def invoke_stream( + self, messages: List[Message], tool_results: Optional[List[ChatRequestToolResultsItem]] = None + ) -> Iterator[StreamedChatResponse]: + api_kwargs: Dict[str, Any] = self.api_kwargs + chat_message: Optional[str] = None + + if self.add_chat_history: + logger.debug("Providing chat_history to cohere") + chat_history = [] + for m in messages: + if m.role == "system" and "preamble" not in api_kwargs: + api_kwargs["preamble"] = m.get_content_string() + elif m.role == "user": + if chat_message is not None: + # Add the existing chat_message to the chat_history + chat_history.append({"role": "USER", "message": chat_message}) + # Update the chat_message to the new user message + chat_message = m.get_content_string() + else: + chat_history.append({"role": "CHATBOT", "message": m.get_content_string() or ""}) + api_kwargs["chat_history"] = chat_history + else: + # Set first system message as preamble + for m in messages: + if m.role == "system" and "preamble" not in api_kwargs: + api_kwargs["preamble"] = m.get_content_string() + break + # Set last user message as chat_message + for m in reversed(messages): + if m.role == "user": + chat_message = m.get_content_string() + break + + if self.tools: + api_kwargs["tools"] = self.get_tools() + + if tool_results: + api_kwargs["tool_results"] = tool_results + + logger.debug(f"Chat message: {chat_message}") + return self.client.chat_stream(message=chat_message or "", model=self.model, **api_kwargs) + + def response(self, messages: List[Message], tool_results: Optional[List[ChatRequestToolResultsItem]] = None) -> str: + logger.debug("---------- Cohere Response Start ----------") + # -*- Log messages for debugging + for m in messages: + m.log() + + response_timer = Timer() + response_timer.start() + response: NonStreamedChatResponse = self.invoke(messages=messages, tool_results=tool_results) + response_timer.stop() + logger.debug(f"Time to generate response: {response_timer.elapsed:.4f}s") + + # -*- Parse response + response_content = response.text + response_tool_calls: Optional[List[CohereToolCall]] = response.tool_calls + + # -*- Create assistant message + assistant_message = Message(role="assistant", content=response_content) + + # -*- Get tool calls from response + if response_tool_calls: + tool_calls: List[Dict[str, Any]] = [] + for tools in response_tool_calls: + tool_calls.append( + { + "type": "function", + "function": { + "name": tools.name, + "arguments": json.dumps(tools.parameters), + }, + } + ) + if len(tool_calls) > 0: + assistant_message.tool_calls = tool_calls + + # -*- Update usage metrics + # Add response time to metrics + assistant_message.metrics["time"] = response_timer.elapsed + if "response_times" not in self.metrics: + self.metrics["response_times"] = [] + self.metrics["response_times"].append(response_timer.elapsed) + + # -*- Add assistant message to messages + messages.append(assistant_message) + assistant_message.log() + + # -*- Run function call + if assistant_message.tool_calls is not None and self.run_tools: + final_response = "" + function_calls_to_run: List[FunctionCall] = [] + for tool_call in assistant_message.tool_calls: + _function_call = get_function_call_for_tool_call(tool_call, self.functions) + if _function_call is None: + messages.append(Message(role="user", content="Could not find function to call.")) + continue + if _function_call.error is not None: + messages.append(Message(role="user", content=_function_call.error)) + continue + function_calls_to_run.append(_function_call) + + if self.show_tool_calls: + if len(function_calls_to_run) == 1: + final_response += f" - Running: {function_calls_to_run[0].get_call_str()}\n\n" + elif len(function_calls_to_run) > 1: + final_response += "Running:" + for _f in function_calls_to_run: + final_response += f"\n - {_f.get_call_str()}" + final_response += "\n\n" + + function_call_results = self.run_function_calls(function_calls_to_run, role="user") + + # Making sure the length of tool calls and function call results are the same to avoid unexpected behavior + if response_tool_calls is not None and 0 < len(function_call_results) == len(response_tool_calls): + # Constructs a list named tool_results, where each element is a dictionary that contains details of tool calls and their outputs. + # It pairs each tool call in response_tool_calls with its corresponding result in function_call_results. + tool_results = [ + ChatRequestToolResultsItem( + call=tool_call, outputs=[tool_call.parameters, {"result": fn_result.content}] + ) + for tool_call, fn_result in zip(response_tool_calls, function_call_results) + ] + messages.append(Message(role="user", content="Tool result")) + # logger.debug(f"Tool results: {tool_results}") + + # -*- Yield new response using results of tool calls + final_response += self.response(messages=messages, tool_results=tool_results) + return final_response + logger.debug("---------- Cohere Response End ----------") + # -*- Return content if no function calls are present + if assistant_message.content is not None: + return assistant_message.get_content_string() + return "Something went wrong, please try again." + + def response_stream( + self, messages: List[Message], tool_results: Optional[List[ChatRequestToolResultsItem]] = None + ) -> Any: + logger.debug("---------- Cohere Response Start ----------") + # -*- Log messages for debugging + for m in messages: + m.log() + + assistant_message_content = "" + tool_calls: List[Dict[str, Any]] = [] + response_tool_calls: List[CohereToolCall] = [] + response_timer = Timer() + response_timer.start() + for response in self.invoke_stream(messages=messages, tool_results=tool_results): + # logger.debug(f"Cohere response type: {type(response)}") + # logger.debug(f"Cohere response: {response}") + + if isinstance(response, StreamedChatResponse_StreamStart): + pass + + if isinstance(response, StreamedChatResponse_TextGeneration): + if response.text is not None: + assistant_message_content += response.text + + yield response.text + + # Detect if response is a tool call + if isinstance(response, StreamedChatResponse_ToolCallsGeneration): + for tc in response.tool_calls: + response_tool_calls.append(tc) + tool_calls.append( + { + "type": "function", + "function": { + "name": tc.name, + "arguments": json.dumps(tc.parameters), + }, + } + ) + + response_timer.stop() + logger.debug(f"Time to generate response: {response_timer.elapsed:.4f}s") + + # -*- Create assistant message + assistant_message = Message(role="assistant", content=assistant_message_content) + # -*- Add tool calls to assistant message + if len(tool_calls) > 0: + assistant_message.tool_calls = tool_calls + + # -*- Update usage metrics + # Add response time to metrics + assistant_message.metrics["time"] = response_timer.elapsed + if "response_times" not in self.metrics: + self.metrics["response_times"] = [] + self.metrics["response_times"].append(response_timer.elapsed) + + # -*- Add assistant message to messages + messages.append(assistant_message) + assistant_message.log() + + # -*- Parse and run function call + if assistant_message.tool_calls is not None and self.run_tools: + function_calls_to_run: List[FunctionCall] = [] + for tool_call in assistant_message.tool_calls: + _function_call = get_function_call_for_tool_call(tool_call, self.functions) + if _function_call is None: + messages.append(Message(role="user", content="Could not find function to call.")) + continue + if _function_call.error is not None: + messages.append(Message(role="user", content=_function_call.error)) + continue + function_calls_to_run.append(_function_call) + + if self.show_tool_calls: + if len(function_calls_to_run) == 1: + yield f"- Running: {function_calls_to_run[0].get_call_str()}\n\n" + elif len(function_calls_to_run) > 1: + yield "Running:" + for _f in function_calls_to_run: + yield f"\n - {_f.get_call_str()}" + yield "\n\n" + + function_call_results = self.run_function_calls(function_calls_to_run, role="user") + + # Making sure the length of tool calls and function call results are the same to avoid unexpected behavior + if response_tool_calls is not None and 0 < len(function_call_results) == len(tool_calls): + # Constructs a list named tool_results, where each element is a dictionary that contains details of tool calls and their outputs. + # It pairs each tool call in response_tool_calls with its corresponding result in function_call_results. + tool_results = [ + ChatRequestToolResultsItem( + call=tool_call, outputs=[tool_call.parameters, {"result": fn_result.content}] + ) + for tool_call, fn_result in zip(response_tool_calls, function_call_results) + ] + messages.append(Message(role="user", content="Tool result")) + # logger.debug(f"Tool results: {tool_results}") + + # -*- Yield new response using results of tool calls + yield from self.response_stream(messages=messages, tool_results=tool_results) + logger.debug("---------- Cohere Response End ----------") + + def get_tool_call_prompt(self) -> Optional[str]: + if self.functions is not None and len(self.functions) > 0: + preamble = """\ + ## Task & Context + You help people answer their questions and other requests interactively. You will be asked a very wide array of requests on all kinds of topics. You will be equipped with a wide range of search engines or similar tools to help you, which you use to research your answer. You should focus on serving the user's needs as best you can, which will be wide-ranging. + + + ## Style Guide + Unless the user asks for a different style of answer, you should answer in full sentences, using proper grammar and spelling. + + """ + return dedent(preamble) + + return None + + def get_system_prompt_from_llm(self) -> Optional[str]: + return self.get_tool_call_prompt() diff --git a/phi/llm/openai/chat.py b/phi/llm/openai/chat.py index 6ae86e28ef..adf80c9a2a 100644 --- a/phi/llm/openai/chat.py +++ b/phi/llm/openai/chat.py @@ -57,7 +57,7 @@ class OpenAIChat(LLM): default_headers: Optional[Any] = None default_query: Optional[Any] = None client_params: Optional[Dict[str, Any]] = None - # -*- Provide the OpenAIClient manually + # -*- Provide the OpenAI client manually openai_client: Optional[OpenAIClient] = None @property @@ -603,6 +603,11 @@ def response_stream(self, messages: List[Message]) -> Iterator[str]: function_call_results = self.run_function_calls(function_calls_to_run) if len(function_call_results) > 0: messages.extend(function_call_results) + # Code to show function call results + # for f in function_call_results: + # yield "\n" + # yield f.get_content_string() + # yield "\n" # -*- Yield new response using results of tool calls yield from self.response_stream(messages=messages) logger.debug("---------- OpenAI Response End ----------") diff --git a/phi/task/llm/llm_task.py b/phi/task/llm/llm_task.py index 7f590f52cc..db5c13496c 100644 --- a/phi/task/llm/llm_task.py +++ b/phi/task/llm/llm_task.py @@ -112,6 +112,8 @@ class LLMTask(Task): # If True, add the current datetime to the prompt to give the assistant a sense of time # This allows for relative times like "tomorrow" to be used in the prompt add_datetime_to_instructions: bool = False + # Add instructions for delegating tasks to another assistant + add_delegation_instructions: bool = True # If markdown=true, add instructions to format the output using markdown markdown: bool = False @@ -150,6 +152,8 @@ class LLMTask(Task): # ... chat_history_function: Optional[Callable[..., Optional[str]]] = None + delegation_prompt: Optional[str] = None + @property def streamable(self) -> bool: return self.output_model is None @@ -167,19 +171,21 @@ def set_default_llm(self) -> None: self.llm = OpenAIChat() - def add_response_format_to_llm(self) -> None: - if self.output_model is not None and self.llm is not None: - self.llm.response_format = {"type": "json_object"} - - def add_tools_to_llm(self) -> None: + def update_llm(self) -> None: if self.llm is None: logger.error(f"Task LLM is None: {self.__class__.__name__}") return + # Set response_format if it is not set on the llm + if self.output_model is not None and self.llm.response_format is None: + self.llm.response_format = {"type": "json_object"} + + # Add tools to the LLM if self.tools is not None: for tool in self.tools: self.llm.add_tool(tool) + # Add default tools to the LLM if self.use_tools: if self.read_chat_history_tool and self.memory is not None: self.llm.add_tool(self.get_chat_history) @@ -203,11 +209,13 @@ def add_tools_to_llm(self) -> None: if self.tool_call_limit is not None and self.tool_call_limit < self.llm.function_call_limit: self.llm.function_call_limit = self.tool_call_limit + if self.run_id is not None: + self.llm.run_id = self.run_id + def prepare_task(self) -> None: self.set_task_id() self.set_default_llm() - self.add_response_format_to_llm() - self.add_tools_to_llm() + self.update_llm() def get_json_output_prompt(self) -> str: json_output_prompt = "\nProvide your output as a JSON containing the following fields:" @@ -312,6 +320,13 @@ def get_system_prompt(self) -> Optional[str]: # Add default instructions if _instructions is None: _instructions = [] + # Add instructions for delegating tasks to another assistant + if self.delegation_prompt and self.add_delegation_instructions: + _instructions.append( + "You are the leader of a team of AI Assistants. You can either respond directly or " + "delegate tasks to the assistants below depending on their role and the tools " + "available to them." + ) # Add instructions for using the knowledge base if self.add_references_to_prompt: _instructions.append("Use the information from the knowledge base to help respond to the message") @@ -380,6 +395,10 @@ def get_system_prompt(self) -> Optional[str]: if self.add_to_system_prompt is not None: _system_prompt += "\n" + self.add_to_system_prompt + # Then add the delegation_prompt to the system prompt + if self.delegation_prompt is not None: + _system_prompt += "\n\n" + self.delegation_prompt + # Then add the json output prompt if output_model is set if self.output_model is not None: _system_prompt += "\n" + self.get_json_output_prompt() diff --git a/phi/tools/apify.py b/phi/tools/apify.py new file mode 100644 index 0000000000..77602d672f --- /dev/null +++ b/phi/tools/apify.py @@ -0,0 +1,113 @@ +from os import getenv +from typing import List, Optional + +from phi.tools import Toolkit +from phi.utils.log import logger + +try: + from apify_client import ApifyClient +except ImportError: + raise ImportError("`apify_client` not installed. Please install using `pip install apify-client`") + + +class ApifyTools(Toolkit): + def __init__( + self, + api_key: Optional[str] = None, + website_content_crawler: bool = True, + web_scraper: bool = False, + ): + super().__init__(name="apify_tools") + + self.api_key = api_key or getenv("MY_APIFY_TOKEN") + if not self.api_key: + logger.error("No Apify API key provided") + + if website_content_crawler: + self.register(self.website_content_crawler) + if web_scraper: + self.register(self.web_scrapper) + + def website_content_crawler(self, urls: List[str], timeout: Optional[int] = 60) -> str: + if self.api_key is None: + return "No API key provided" + + if urls is None: + return "No URLs provided" + + client = ApifyClient(self.api_key) + + logger.debug(f"Crawling URLs: {urls}") + + formatted_urls = [{"url": url} for url in urls] + + run_input = {"startUrls": formatted_urls} + + run = client.actor("apify/website-content-crawler").call(run_input=run_input, timeout_secs=timeout) + + results: str = "" + + for item in client.dataset(run["defaultDatasetId"]).iterate_items(): + results += "Results for URL: " + item.get("url") + "\n" + results += item.get("text") + "\n" + + return results + + def web_scrapper(self, urls: List[str], timeout: Optional[int] = 60) -> str: + """ + Scrapes a website using Apify's web-scraper actor. + + :param urls: The URLs to scrape. + :param timeout: The timeout for the scraping. + + :return: The results of the scraping. + """ + if self.api_key is None: + return "No API key provided" + + if urls is None: + return "No URLs provided" + + client = ApifyClient(self.api_key) + + logger.debug(f"Scrapping URLs: {urls}") + + formatted_urls = [{"url": url} for url in urls] + + page_function_string = """ + async function pageFunction(context) { + const $ = context.jQuery; + const pageTitle = $('title').first().text(); + const h1 = $('h1').first().text(); + const first_h2 = $('h2').first().text(); + const random_text_from_the_page = $('p').first().text(); + + context.log.info(`URL: ${context.request.url}, TITLE: ${pageTitle}`); + + return { + url: context.request.url, + pageTitle, + h1, + first_h2, + random_text_from_the_page + }; + } + """ + + run_input = { + "pageFunction": page_function_string, + "startUrls": formatted_urls, + } + + run = client.actor("apify/web-scraper").call(run_input=run_input, timeout_secs=timeout) + + results: str = "" + + for item in client.dataset(run["defaultDatasetId"]).iterate_items(): + results += "Results for URL: " + item.get("url") + "\n" + results += item.get("pageTitle") + "\n" + results += item.get("h1") + "\n" + results += item.get("first_h2") + "\n" + results += item.get("random_text_from_the_page") + "\n" + + return results diff --git a/phi/tools/file.py b/phi/tools/file.py index 1046bee06e..352b1a4da2 100644 --- a/phi/tools/file.py +++ b/phi/tools/file.py @@ -11,7 +11,7 @@ def __init__( base_dir: Optional[Path] = None, save_files: bool = True, read_files: bool = True, - list_files: bool = False, + list_files: bool = True, ): super().__init__(name="file_tools") diff --git a/phi/tools/function.py b/phi/tools/function.py index 835d36f3a1..afea78b348 100644 --- a/phi/tools/function.py +++ b/phi/tools/function.py @@ -58,11 +58,16 @@ def get_definition_for_prompt(self) -> Optional[str]: return None type_hints = get_type_hints(self.entrypoint) + return_type = type_hints.get("return", None) + returns = None + if return_type is not None: + returns = self.get_type_name(return_type) + function_info = { "name": self.name, "description": self.description, "arguments": self.parameters.get("properties", {}), - "returns": type_hints.get("return", "void").__name__, + "returns": returns, } return json.dumps(function_info, indent=2) @@ -73,11 +78,16 @@ def get_definition_for_prompt_dict(self) -> Optional[Dict[str, Any]]: return None type_hints = get_type_hints(self.entrypoint) + return_type = type_hints.get("return", None) + returns = None + if return_type is not None: + returns = self.get_type_name(return_type) + function_info = { "name": self.name, "description": self.description, "arguments": self.parameters.get("properties", {}), - "returns": type_hints.get("return", "void").__name__, + "returns": returns, } return function_info @@ -128,7 +138,7 @@ def execute(self) -> bool: return True except Exception as e: logger.warning(f"Could not run function {self.get_call_str()}") - logger.error(e) + logger.exception(e) self.result = str(e) return False @@ -137,6 +147,6 @@ def execute(self) -> bool: return True except Exception as e: logger.warning(f"Could not run function {self.get_call_str()}") - logger.error(e) + logger.exception(e) self.result = str(e) return False diff --git a/phi/tools/newspaper_toolkit.py b/phi/tools/newspaper_toolkit.py new file mode 100644 index 0000000000..a474e627a4 --- /dev/null +++ b/phi/tools/newspaper_toolkit.py @@ -0,0 +1,35 @@ +from phi.tools import Toolkit + +try: + from newspaper import Article +except ImportError: + raise ImportError("`newspaper3k` not installed.") + + +class NewspaperToolkit(Toolkit): + def __init__( + self, + get_article_text: bool = True, + ): + super().__init__(name="newspaper_toolkit") + + if get_article_text: + self.register(self.get_article_text) + + def get_article_text(self, url: str) -> str: + """Get the text of an article from a URL. + + Args: + url (str): The URL of the article. + + Returns: + str: The text of the article. + """ + + try: + article = Article(url) + article.download() + article.parse() + return article.text + except Exception as e: + return f"Error getting article text from {url}: {e}" diff --git a/phi/tools/serpapi_toolkit.py b/phi/tools/serpapi_toolkit.py new file mode 100644 index 0000000000..5d19dc173a --- /dev/null +++ b/phi/tools/serpapi_toolkit.py @@ -0,0 +1,110 @@ +import json +from os import getenv +from typing import Optional + +from phi.tools import Toolkit +from phi.utils.log import logger + +try: + import serpapi +except ImportError: + raise ImportError("`google-search-results` not installed.") + + +class SerpApiToolkit(Toolkit): + def __init__( + self, + api_key: Optional[str] = getenv("SERPAPI_KEY"), + search_youtube: bool = False, + ): + super().__init__(name="serpapi_tools") + + self.api_key = api_key + if not self.api_key: + logger.warning("No Serpapi API key provided") + + self.register(self.search_google) + if search_youtube: + self.register(self.seach_youtube) + + def search_google(self, query: str) -> str: + """ + Search Google using the Serpapi API. Returns the search results. + + Args: + query(str): The query to search for. + + Returns: + str: The search results from Google. + Keys: + - 'search_results': List of organic search results. + - 'recipes_results': List of recipes search results. + - 'shopping_results': List of shopping search results. + - 'knowledge_graph': The knowledge graph. + - 'related_questions': List of related questions. + """ + + try: + if not self.api_key: + return "Please provide an API key" + if not query: + return "Please provide a query to search for" + + logger.info(f"Searching Google for: {query}") + + params = {"q": query, "api_key": self.api_key} + + search = serpapi.GoogleSearch(params) + results = search.get_dict() + + filtered_results = { + "search_results": results.get("organic_results", ""), + "recipes_results": results.get("recipes_results", ""), + "shopping_results": results.get("shopping_results", ""), + "knowledge_graph": results.get("knowledge_graph", ""), + "related_questions": results.get("related_questions", ""), + } + + return json.dumps(filtered_results) + + except Exception as e: + return f"Error searching for the query {query}: {e}" + + def seach_youtube(self, query: str) -> str: + """ + Search Youtube using the Serpapi API. Returns the search results. + + Args: + query(str): The query to search for. + + Returns: + str: The video search results from Youtube. + Keys: + - 'video_results': List of video results. + - 'movie_results': List of movie results. + - 'channel_results': List of channel results. + """ + + try: + if not self.api_key: + return "Please provide an API key" + if not query: + return "Please provide a query to search for" + + logger.info(f"Searching Youtube for: {query}") + + params = {"search_query": query, "api_key": self.api_key} + + search = serpapi.YoutubeSearch(params) + results = search.get_dict() + + filtered_results = { + "video_results": results.get("video_results", ""), + "movie_results": results.get("movie_results", ""), + "channel_results": results.get("channel_results", ""), + } + + return json.dumps(filtered_results) + + except Exception as e: + return f"Error searching for the query {query}: {e}" diff --git a/phi/vectordb/pineconedb/pineconedb.py b/phi/vectordb/pineconedb/pineconedb.py index f818e2eaf9..a32a77554d 100644 --- a/phi/vectordb/pineconedb/pineconedb.py +++ b/phi/vectordb/pineconedb/pineconedb.py @@ -1,13 +1,15 @@ -from typing import Optional, Dict, Config, Union, List +from typing import Optional, Dict, Union, List try: from pinecone import Pinecone + from pinecone.config import Config except ImportError: raise ImportError( - "The `pinecone-client` package is not installed. " "Please install it via `pip install pinecone-client`." + "The `pinecone-client` package is not installed, please install using `pip install pinecone-client`." ) from phi.document import Document +from phi.embedder import Embedder from phi.vectordb.base import VectorDb from phi.utils.log import logger from pinecone.core.client.api.manage_indexes_api import ManageIndexesApi @@ -46,7 +48,6 @@ class PineconeDB(VectorDb): metric (Optional[str]): The metric used for similarity search. timeout (Optional[int]): The timeout for Pinecone operations. kwargs (Optional[Dict[str, str]]): Additional keyword arguments. - """ def __init__( @@ -54,8 +55,9 @@ def __init__( name: str, dimension: int, spec: Union[Dict, ServerlessSpec, PodSpec], + embedder: Optional[Embedder] = None, metric: Optional[str] = "cosine", - additional_headers: Optional[Dict[str, str]] = {}, + additional_headers: Optional[Dict[str, str]] = None, pool_threads: Optional[int] = 1, namespace: Optional[str] = None, timeout: Optional[int] = None, @@ -70,7 +72,7 @@ def __init__( self.api_key: Optional[str] = api_key self.host: Optional[str] = host self.config: Optional[Config] = config - self.additional_headers: Optional[Dict[str, str]] = additional_headers + self.additional_headers: Dict[str, str] = additional_headers or {} self.pool_threads: Optional[int] = pool_threads self.namespace: Optional[str] = namespace self.index_api: Optional[ManageIndexesApi] = index_api @@ -81,6 +83,14 @@ def __init__( self.timeout: Optional[int] = timeout self.kwargs: Optional[Dict[str, str]] = kwargs + # Embedder for embedding the document contents + _embedder = embedder + if _embedder is None: + from phi.embedder.openai import OpenAIEmbedder + + _embedder = OpenAIEmbedder() + self.embedder: Embedder = _embedder + @property def client(self) -> Pinecone: """The Pinecone client. @@ -188,7 +198,7 @@ def upsert( show_progress (bool, optional): Whether to show progress during upsert. Defaults to False. """ - vectors = [{"id": doc.id, "values": doc.embedding, "metadata": doc.metadata} for doc in documents] + vectors = [{"id": doc.id, "values": doc.embedding, "metadata": doc.meta_data} for doc in documents] self.index.upsert( vectors=vectors, namespace=namespace, @@ -221,7 +231,7 @@ def insert(self, documents: List[Document]) -> None: def search( self, - query_vector: List[float], + query: str, limit: int = 5, namespace: Optional[str] = None, filter: Optional[Dict[str, Union[str, float, int, bool, List, dict]]] = None, @@ -231,7 +241,7 @@ def search( """Search for similar documents in the index. Args: - query_vector (List[float]): The query vector. + query (str): The query to search for. limit (int, optional): The maximum number of results to return. Defaults to 5. namespace (Optional[str], optional): The namespace to search in. Defaults to None. filter (Optional[Dict[str, Union[str, float, int, bool, List, dict]]], optional): The filter for the search. Defaults to None. @@ -242,8 +252,13 @@ def search( List[Document]: The list of matching documents. """ + query_embedding = self.embedder.get_embedding(query) + if query_embedding is None: + logger.error(f"Error getting embedding for Query: {query}") + return [] + response = self.index.query( - vector=query_vector, + vector=query_embedding, top_k=limit, namespace=namespace, filter=filter, @@ -251,7 +266,13 @@ def search( include_metadata=include_metadata, ) return [ - Document(id=result.id, embedding=result.values, metadata=result.metadata) for result in response.matches + Document( + content=result.metadata.get("text", "") if result.metadata is not None else "", + id=result.id, + embedding=result.values, + meta_data=result.metadata, + ) + for result in response.matches ] def optimize(self) -> None: @@ -262,11 +283,15 @@ def optimize(self) -> None: """ pass - def clear(self, namespace: Optional[str] = None) -> None: + def clear(self, namespace: Optional[str] = None) -> bool: """Clear the index. Args: namespace (Optional[str], optional): The namespace to clear. Defaults to None. """ - self.index.delete(delete_all=True, namespace=namespace) + try: + self.index.delete(delete_all=True, namespace=namespace) + return True + except Exception: + return False diff --git a/pyproject.toml b/pyproject.toml index fae1797ab6..6b425e33ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,9 +78,11 @@ module = [ "altair.*", "arxiv.*", "anthropic.*", + "apify_client.*", "boto3.*", "botocore.*", "bs4.*", + "cohere.*", "docker.*", "duckdb.*", "exa_py.*", @@ -92,16 +94,19 @@ module = [ "langchain.*", "langchain_core.*", "mistralai.*", + "newspaper.*", "numpy.*", "ollama.*", "openai.*", "pandas.*", + "pinecone.*", "pyarrow.*", "pgvector.*", "psycopg.*", "pypdf.*", "qdrant_client.*", "simplejson.*", + "serpapi.*", "setuptools.*", "sqlalchemy.*", "streamlit.*",