From 469fc0a30ab3f31951086e04d67ce6b80c949c4b Mon Sep 17 00:00:00 2001 From: Ashpreet Bedi Date: Fri, 10 Nov 2023 13:19:46 +0000 Subject: [PATCH 1/5] v2.0.36 --- phi/ai/phi_ai.py | 13 +- phi/api/ai.py | 3 +- phi/assistant/assistant.py | 139 +++-------- phi/assistant/run.py | 13 +- phi/assistant/tool/__init__.py | 1 - phi/conversation/conversation.py | 116 +++------ phi/llm/agent/__init__.py | 0 phi/llm/agent/arxiv.py | 53 ---- phi/llm/agent/base.py | 27 -- phi/llm/agent/duckdb.py | 339 -------------------------- phi/llm/agent/email.py | 59 ----- phi/llm/agent/google.py | 18 -- phi/llm/agent/phi.py | 116 --------- phi/llm/agent/pubmed.py | 19 -- phi/llm/agent/shell.py | 34 --- phi/llm/agent/website.py | 50 ---- phi/llm/agent/wikipedia.py | 54 ---- phi/llm/base.py | 150 ++++-------- phi/llm/function/__init__.py | 0 phi/llm/function/arxiv.py | 53 ---- phi/llm/function/email.py | 59 ----- phi/llm/function/google.py | 18 -- phi/llm/function/phi_commands.py | 116 --------- phi/llm/function/pubmed.py | 19 -- phi/llm/function/registry.py | 27 -- phi/llm/function/shell.py | 34 --- phi/llm/function/website.py | 50 ---- phi/llm/function/wikipedia.py | 54 ---- phi/llm/openai/chat.py | 53 ++-- phi/llm/schemas.py | 91 +------ phi/llm/task/llm_task.py | 96 +++----- phi/tool/__init__.py | 1 + phi/{assistant => }/tool/arxiv.py | 4 +- phi/{assistant => }/tool/duckdb.py | 4 +- phi/{assistant => }/tool/email.py | 4 +- phi/{assistant => tool}/function.py | 15 +- phi/{assistant => }/tool/phi.py | 4 +- phi/{assistant => }/tool/registry.py | 4 +- phi/{assistant => }/tool/shell.py | 4 +- phi/{assistant => }/tool/tool.py | 3 +- phi/{assistant => }/tool/website.py | 4 +- phi/{assistant => }/tool/wikipedia.py | 4 +- phi/utils/functions.py | 2 +- 43 files changed, 227 insertions(+), 1700 deletions(-) delete mode 100644 phi/assistant/tool/__init__.py delete mode 100644 phi/llm/agent/__init__.py delete mode 100644 phi/llm/agent/arxiv.py delete mode 100644 phi/llm/agent/base.py delete mode 100644 phi/llm/agent/duckdb.py delete mode 100644 phi/llm/agent/email.py delete mode 100644 phi/llm/agent/google.py delete mode 100644 phi/llm/agent/phi.py delete mode 100644 phi/llm/agent/pubmed.py delete mode 100644 phi/llm/agent/shell.py delete mode 100644 phi/llm/agent/website.py delete mode 100644 phi/llm/agent/wikipedia.py delete mode 100644 phi/llm/function/__init__.py delete mode 100644 phi/llm/function/arxiv.py delete mode 100644 phi/llm/function/email.py delete mode 100644 phi/llm/function/google.py delete mode 100644 phi/llm/function/phi_commands.py delete mode 100644 phi/llm/function/pubmed.py delete mode 100644 phi/llm/function/registry.py delete mode 100644 phi/llm/function/shell.py delete mode 100644 phi/llm/function/website.py delete mode 100644 phi/llm/function/wikipedia.py create mode 100644 phi/tool/__init__.py rename phi/{assistant => }/tool/arxiv.py (96%) rename phi/{assistant => }/tool/duckdb.py (99%) rename phi/{assistant => }/tool/email.py (96%) rename phi/{assistant => tool}/function.py (90%) rename phi/{assistant => }/tool/phi.py (98%) rename phi/{assistant => }/tool/registry.py (89%) rename phi/{assistant => }/tool/shell.py (92%) rename phi/{assistant => }/tool/tool.py (77%) rename phi/{assistant => }/tool/website.py (95%) rename phi/{assistant => }/tool/wikipedia.py (96%) diff --git a/phi/ai/phi_ai.py b/phi/ai/phi_ai.py index 502c92c14..a86c605d8 100644 --- a/phi/ai/phi_ai.py +++ b/phi/ai/phi_ai.py @@ -13,9 +13,10 @@ from phi.cli.config import PhiCliConfig from phi.cli.console import console from phi.cli.settings import phi_cli_settings -from phi.llm.schemas import Function, Message, FunctionCall -from phi.llm.function.shell import ShellScriptsRegistry -from phi.llm.function.phi_commands import PhiCommandsRegistry +from phi.llm.schemas import Message +from phi.tool.function import Function, FunctionCall +from phi.tool.shell import ShellTool +from phi.tool.phi import PhiTool from phi.workspace.config import WorkspaceConfig from phi.utils.log import logger from phi.utils.functions import get_function_call @@ -41,7 +42,7 @@ def __init__( _active_workspace = _phi_config.get_active_ws_config() self.conversation_db: Optional[List[Dict[str, Any]]] = None - self.functions: Dict[str, Function] = {**ShellScriptsRegistry().functions, **PhiCommandsRegistry().functions} + self.functions: Dict[str, Function] = {**ShellTool().functions, **PhiTool().functions} _conversation_id = None _conversation_history = None @@ -170,7 +171,7 @@ def run_function_stream(self, function_call: Dict[str, Any]) -> Iterator[str]: yield f"Running: {function_call_obj.get_call_str()}\n\n" function_call_timer = Timer() function_call_timer.start() - function_call_obj.run() + function_call_obj.execute() function_call_timer.stop() function_call_message = Message( role="function", @@ -213,7 +214,7 @@ def run_function(self, function_call: Dict[str, Any]) -> str: function_run_response = f"Running: {function_call_obj.get_call_str()}\n\n" function_call_timer = Timer() function_call_timer.start() - function_call_obj.run() + function_call_obj.execute() function_call_timer.stop() function_call_message = Message( role="function", diff --git a/phi/api/ai.py b/phi/api/ai.py index e1dae5e1d..99b950e2f 100644 --- a/phi/api/ai.py +++ b/phi/api/ai.py @@ -10,7 +10,8 @@ ConversationCreateResponse, ) from phi.api.schemas.user import UserSchema -from phi.llm.schemas import Function, Message +from phi.llm.schemas import Message +from phi.tool.function import Function from phi.utils.log import logger diff --git a/phi/assistant/assistant.py b/phi/assistant/assistant.py index 29ca74d59..8f550feeb 100644 --- a/phi/assistant/assistant.py +++ b/phi/assistant/assistant.py @@ -1,16 +1,16 @@ +import json from typing import List, Any, Optional, Dict, Union, Callable from pydantic import BaseModel, ConfigDict, field_validator, model_validator from phi.assistant.file import File -from phi.assistant.tool import Tool -from phi.assistant.tool.registry import ToolRegistry from phi.assistant.row import AssistantRow -from phi.assistant.function import Function, FunctionCall from phi.assistant.storage import AssistantStorage from phi.assistant.exceptions import AssistantIdNotSet +from phi.tool import Tool +from phi.tool.registry import ToolRegistry +from phi.tool.function import Function from phi.knowledge.base import KnowledgeBase - from phi.utils.log import logger, set_log_level_to_debug try: @@ -43,8 +43,9 @@ class Assistant(BaseModel): # A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant. # Tools can be of types code_interpreter, retrieval, or function. tools: Optional[List[Union[Tool, Dict, Callable, ToolRegistry]]] = None - # Functions the Assistant may call. - _function_map: Optional[Dict[str, Function]] = None + # -*- Functions available to the Assistant to call + # Functions provided from the tools. Note: These are not sent to the LLM API. + functions: Optional[Dict[str, Function]] = None # -*- Assistant Files # A list of file IDs attached to this assistant. @@ -94,24 +95,19 @@ def set_log_level(cls, v: bool) -> bool: def client(self) -> OpenAI: return self.openai or OpenAI() - def add_function(self, f: Function) -> None: - if self._function_map is None: - self._function_map = {} - self._function_map[f.name] = f - logger.debug(f"Added function {f.name} to Assistant") - @model_validator(mode="after") - def add_functions_to_assistant(self) -> "Assistant": + def extract_functions_from_tools(self) -> "Assistant": if self.tools is not None: for tool in self.tools: - if callable(tool): - f = Function.from_callable(tool) - self.add_function(f) - elif isinstance(tool, ToolRegistry): - if self._function_map is None: - self._function_map = {} - self._function_map.update(tool.functions) + if self.functions is None: + self.functions = {} + if isinstance(tool, ToolRegistry): + self.functions.update(tool.functions) logger.debug(f"Tools from {tool.name} added to Assistant.") + elif callable(tool): + f = Function.from_callable(tool) + self.functions[f.name] = f + logger.debug(f"Added function {f.name} to Assistant") return self def load_from_storage(self): @@ -124,6 +120,24 @@ def load_from_openai(self, openai_assistant: OpenAIAssistant): self.file_ids = openai_assistant.file_ids self.openai_assistant = openai_assistant + def get_tools_for_api(self) -> Optional[List[Dict[str, Any]]]: + if self.tools is None: + return None + + tools_for_api = [] + for tool in self.tools: + if isinstance(tool, Tool): + tools_for_api.append(tool.to_dict()) + elif isinstance(tool, dict): + tools_for_api.append(tool) + elif callable(tool): + func = Function.from_callable(tool) + tools_for_api.append({"type": "function", "function": func.to_dict()}) + elif isinstance(tool, ToolRegistry): + for _f in tool.functions.values(): + tools_for_api.append({"type": "function", "function": _f.to_dict()}) + return tools_for_api + def create(self) -> "Assistant": request_body: Dict[str, Any] = {} if self.name is not None: @@ -133,19 +147,7 @@ def create(self) -> "Assistant": if self.instructions is not None: request_body["instructions"] = self.instructions if self.tools is not None: - _tools = [] - for _tool in self.tools: - if isinstance(_tool, Tool): - _tools.append(_tool.to_dict()) - elif isinstance(_tool, dict): - _tools.append(_tool) - elif callable(_tool): - func = Function.from_callable(_tool) - _tools.append({"type": "function", "function": func.to_dict()}) - elif isinstance(_tool, ToolRegistry): - for _f in _tool.functions.values(): - _tools.append({"type": "function", "function": _f.to_dict()}) - request_body["tools"] = _tools + request_body["tools"] = self.get_tools_for_api() if self.file_ids is not None or self.files is not None: _file_ids = self.file_ids or [] if self.files is not None: @@ -208,19 +210,7 @@ def update(self) -> "Assistant": if self.instructions is not None: request_body["instructions"] = self.instructions if self.tools is not None: - _tools = [] - for _tool in self.tools: - if isinstance(_tool, Tool): - _tools.append(_tool.to_dict()) - elif isinstance(_tool, dict): - _tools.append(_tool) - elif callable(_tool): - func = Function.from_callable(_tool) - _tools.append({"type": "function", "function": func.to_dict()}) - elif isinstance(_tool, ToolRegistry): - for _f in _tool.functions.values(): - _tools.append({"type": "function", "function": _f.to_dict()}) - request_body["tools"] = _tools + request_body["tools"] = self.get_tools_for_api() if self.file_ids is not None or self.files is not None: _file_ids = self.file_ids or [] if self.files is not None: @@ -286,61 +276,4 @@ def pprint(self): pprint(self.to_dict()) def __str__(self) -> str: - import json - return json.dumps(self.to_dict(), indent=4) - - def get_function_call(self, name: str, arguments: Optional[str] = None) -> Optional[FunctionCall]: - import json - - logger.debug(f"Getting function {name}. Args: {arguments}") - if self._function_map is None: - return None - - function_to_call: Optional[Function] = None - if name in self._function_map: - function_to_call = self._function_map[name] - if function_to_call is None: - logger.error(f"Function {name} not found") - return None - - function_call = FunctionCall(function=function_to_call) - if arguments is not None and arguments != "": - try: - if "None" in arguments: - arguments = arguments.replace("None", "null") - if "True" in arguments: - arguments = arguments.replace("True", "true") - if "False" in arguments: - arguments = arguments.replace("False", "false") - _arguments = json.loads(arguments) - except Exception as e: - logger.error(f"Unable to decode function arguments {arguments}: {e}") - return None - - if not isinstance(_arguments, dict): - logger.error(f"Function arguments {arguments} is not a valid JSON object") - return None - - try: - clean_arguments: Dict[str, Any] = {} - for k, v in _arguments.items(): - if isinstance(v, str): - _v = v.strip().lower() - if _v in ("none", "null"): - clean_arguments[k] = None - elif _v == "true": - clean_arguments[k] = True - elif _v == "false": - clean_arguments[k] = False - else: - clean_arguments[k] = v.strip() - else: - clean_arguments[k] = v - - function_call.arguments = clean_arguments - except Exception as e: - logger.error(f"Unable to parse function arguments {arguments}: {e}") - return None - - return function_call diff --git a/phi/assistant/run.py b/phi/assistant/run.py index 3138dc922..c0f91675a 100644 --- a/phi/assistant/run.py +++ b/phi/assistant/run.py @@ -3,11 +3,12 @@ from pydantic import BaseModel, ConfigDict, model_validator -from phi.assistant.tool import Tool -from phi.assistant.tool.registry import ToolRegistry -from phi.assistant.function import Function from phi.assistant.assistant import Assistant from phi.assistant.exceptions import ThreadIdNotSet, AssistantIdNotSet, RunIdNotSet +from phi.tool import Tool +from phi.tool.registry import ToolRegistry +from phi.tool.function import Function +from phi.utils.functions import get_function_call from phi.utils.log import logger try: @@ -300,8 +301,10 @@ def run( tool_outputs = [] for tool_call in tool_calls: if tool_call.type == "function": - function_call = self.assistant.get_function_call( - name=tool_call.function.name, arguments=tool_call.function.arguments + function_call = get_function_call( + name=tool_call.function.name, + arguments=tool_call.function.arguments, + functions=self.assistant.functions, ) if function_call is None: logger.error(f"Function {tool_call.function.name} not found") diff --git a/phi/assistant/tool/__init__.py b/phi/assistant/tool/__init__.py deleted file mode 100644 index 01bde82b3..000000000 --- a/phi/assistant/tool/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from phi.assistant.tool.tool import Tool diff --git a/phi/conversation/conversation.py b/phi/conversation/conversation.py index 7690c1e56..30964f0af 100644 --- a/phi/conversation/conversation.py +++ b/phi/conversation/conversation.py @@ -12,9 +12,9 @@ from phi.llm.base import LLM from phi.llm.openai import OpenAIChat from phi.llm.schemas import Message, References -from phi.llm.agent.base import BaseAgent -from phi.llm.function.registry import FunctionRegistry from phi.llm.task.llm_task import LLMTask +from phi.tool.tool import Tool +from phi.tool.registry import ToolRegistry from phi.utils.format_str import remove_indent from phi.utils.log import logger, set_log_level_to_debug from phi.utils.timer import Timer @@ -22,9 +22,7 @@ class Conversation(BaseModel): # -*- LLM settings - # LLM to use for this conversation llm: LLM = OpenAIChat() - # LLM Introduction # Add an introduction (from the LLM) to the chat history introduction: Optional[str] = None @@ -75,19 +73,23 @@ class Conversation(BaseModel): # -*- Enable Function Calls # Makes the conversation Autonomous by letting the LLM call functions to achieve tasks. function_calls: bool = False - # Add a list of default functions to the LLM + # Add default functions to the LLM when function_calls is True. default_functions: bool = True # Show function calls in LLM messages. show_function_calls: bool = False - # A list of functions to add to the LLM. - functions: Optional[List[Callable]] = None - # A list of function registries to add to the LLM. - function_registries: Optional[List[FunctionRegistry]] = None - # -*- Agents - # Add a list of agents to the LLM - # function_calls must be True for agents to be added to the LLM - agents: Optional[List[BaseAgent]] = None + # -*- Conversation Tools + # A list of tools provided to the LLM. + # Currently, only functions are supported as a tool. + # Use this to provide a list of functions the model may generate JSON inputs for. + tools: Optional[List[Union[Tool, Dict, Callable, ToolRegistry]]] = None + # Controls which (if any) function is called by the model. + # "none" means the model will not call a function and instead generates a message. + # "auto" means the model can pick between generating a message or calling a function. + # Specifying a particular function via {"type: "function", "function": {"name": "my_function"}} + # forces the model to call that function. + # "none" is the default when no functions are present. "auto" is the default if functions are present. + tool_choice: Optional[Union[str, Dict[str, Any]]] = None # -*- Tasks # Generate a response using tasks instead of a prompt @@ -96,9 +98,9 @@ class Conversation(BaseModel): # # -*- Prompt Settings # - # -*- System prompt: provide the system prompt as a string or using a function + # -*- System prompt: provide the system prompt as a string system_prompt: Optional[str] = None - # Function to build the system prompt. + # -*- System prompt function: provide the system prompt as a function # This function is provided the conversation as an argument # and should return the system_prompt as a string. # Signature: @@ -108,10 +110,10 @@ class Conversation(BaseModel): # If True, the conversation provides a default system prompt use_default_system_prompt: bool = True - # -*- User prompt: provide the user prompt as a string or using a function + # -*- User prompt: provide the user prompt as a string # Note: this will ignore the message provided to the chat function user_prompt: Optional[Union[List[Dict], str]] = None - # Function to build the user prompt. + # -*- User prompt function: provide the user prompt as a function. # This function is provided the conversation and the user message as arguments # and should return the user_prompt as a Union[List[Dict], str]. # If add_references_to_prompt is True, then references are also provided as an argument. @@ -141,7 +143,7 @@ class Conversation(BaseModel): # ... chat_history_function: Optional[Callable[..., Optional[str]]] = None - # -*- Latest LLM response + # -*- Latest LLM response i.e. the final output of this conversation output: Optional[Any] = None # If True, show debug logs @@ -163,63 +165,27 @@ def set_log_level(cls, v: bool) -> bool: return v @model_validator(mode="after") - def add_functions_to_llm(self) -> "Conversation": - update_tool_choice = False - - # Add functions from self.functions - if self.functions is not None: - update_tool_choice = True - for func in self.functions: - self.llm.add_function(func) - - # Add functions from registries - if self.function_registries is not None: - update_tool_choice = True - for registry in self.function_registries: - self.llm.add_function_registry(registry) - - if self.function_calls: - update_tool_choice = True - if self.default_functions: - default_func_list: List[Callable] = [ - self.get_last_n_chats, - self.search_knowledge_base, - ] - for func in default_func_list: - self.llm.add_function(func) - - # Set function_call/tool_choice to auto if it is not set - if update_tool_choice: - # Set function call to auto if it is not set - if self.llm.function_call is None: - self.llm.function_call = "auto" - # Set tool_choice to auto if it is not set - if self.llm.tool_choice is None: - self.llm.tool_choice = "auto" + def add_tools_to_llm(self) -> "Conversation": + if self.tools is not None: + for tool in self.tools: + self.llm.add_tool(tool) + + if self.function_calls and self.default_functions: + default_func_list: List[Callable] = [ + self.get_last_n_chats, + self.search_knowledge_base, + ] + for func in default_func_list: + self.llm.add_tool(func) # Set show_function_calls if it is not set on the llm - if self.llm.show_function_calls is None: + if self.llm.show_function_calls is None and self.show_function_calls is not None: self.llm.show_function_calls = self.show_function_calls - return self + # Set tool_choice to auto if it is not set on the llm + if self.llm.tool_choice is None and self.tool_choice is not None: + self.llm.tool_choice = self.tool_choice - @model_validator(mode="after") - def add_agents_to_llm(self) -> "Conversation": - if self.agents is not None and len(self.agents) > 0: - for agent in self.agents: - self.llm.add_agent(agent) - - # Set function_call to auto if it is not set - if self.llm.function_call is None: - self.llm.function_call = "auto" - - # Set tool_choice to auto if it is not set - if self.llm.tool_choice is None: - self.llm.tool_choice = "auto" - - # Set show_function_calls if it is not set on the llm - if self.llm.show_function_calls is None: - self.llm.show_function_calls = self.show_function_calls return self def to_database_row(self) -> ConversationRow: @@ -264,16 +230,6 @@ def from_database_row(self, row: ConversationRow): except Exception as e: logger.warning(f"Failed to load llm metrics: {e}") - # # Update llm functions from the database - # llm_functions_from_db = row.llm.get("functions") - # if llm_functions_from_db is not None and isinstance(llm_functions_from_db, dict): - # try: - # for k, v in llm_functions_from_db.items(): - # _llm_function = Function(**v) - # self.llm.add_function_schema(func=_llm_function, if_not_exists=True) - # except Exception as e: - # logger.error(f"Failed to load llm functions: {e}") - # Update conversation memory from the ConversationRow if row.memory is not None: try: diff --git a/phi/llm/agent/__init__.py b/phi/llm/agent/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/phi/llm/agent/arxiv.py b/phi/llm/agent/arxiv.py deleted file mode 100644 index e7b2660bb..000000000 --- a/phi/llm/agent/arxiv.py +++ /dev/null @@ -1,53 +0,0 @@ -import json -from typing import List, Optional - -from phi.document import Document -from phi.knowledge.arxiv import ArxivKnowledgeBase -from phi.llm.agent.base import BaseAgent -from phi.utils.log import logger - - -class ArxivAgent(BaseAgent): - def __init__(self, knowledge_base: Optional[ArxivKnowledgeBase] = None): - super().__init__(name="arxiv_agent") - self.knowledge_base: Optional[ArxivKnowledgeBase] = knowledge_base - - if self.knowledge_base is not None and isinstance(self.knowledge_base, ArxivKnowledgeBase): - self.register(self.search_arxiv_and_update_knowledge_base) - else: - self.register(self.search_arxiv) - - def search_arxiv_and_update_knowledge_base(self, topic: str) -> str: - """This function searches arXiv for a topic, adds the results to the knowledge base and returns them. - - USE THIS FUNCTION TO GET INFORMATION WHICH DOES NOT EXIST. - - :param topic: The topic to search arXiv and add to knowledge base. - :return: Relevant documents from arXiv knowledge base. - """ - if self.knowledge_base is None: - return "Knowledge base not provided" - - logger.debug(f"Adding to knowledge base: {topic}") - self.knowledge_base.queries.append(topic) - logger.debug("Loading knowledge base.") - self.knowledge_base.load(recreate=False) - logger.debug(f"Searching knowledge base: {topic}") - relevant_docs: List[Document] = self.knowledge_base.search(query=topic) - return json.dumps([doc.to_dict() for doc in relevant_docs]) - - def search_arxiv(self, query: str, max_results: int = 5) -> str: - """ - Searches arXiv for a query. - - :param query: The query to search for. - :param max_results: The maximum number of results to return. - :return: Relevant documents from arXiv. - """ - from phi.document.reader.arxiv import ArxivReader - - arxiv = ArxivReader(max_results=max_results) - - logger.debug(f"Searching arxiv for: {query}") - relevant_docs: List[Document] = arxiv.read(query=query) - return json.dumps([doc.to_dict() for doc in relevant_docs]) diff --git a/phi/llm/agent/base.py b/phi/llm/agent/base.py deleted file mode 100644 index 27310a3af..000000000 --- a/phi/llm/agent/base.py +++ /dev/null @@ -1,27 +0,0 @@ -from collections import OrderedDict -from typing import Callable, Dict - -from phi.llm.schemas import Function -from phi.utils.log import logger - - -class BaseAgent: - def __init__(self, name: str = "base_agent"): - self.name: str = name - self.functions: Dict[str, Function] = OrderedDict() - - def register(self, function: Callable): - try: - f = Function.from_callable(function) - self.functions[f.name] = f - logger.debug(f"Function: {f.name} registered with {self.name}") - logger.debug(f"Json Schema: {f.to_dict()}") - except Exception as e: - logger.warning(f"Failed to create Function for: {function.__name__}") - raise e - - def __repr__(self): - return f"<{self.__class__.__name__} name={self.name} functions={list(self.functions.keys())}>" - - def __str__(self): - return self.__repr__() diff --git a/phi/llm/agent/duckdb.py b/phi/llm/agent/duckdb.py deleted file mode 100644 index 9f0abe157..000000000 --- a/phi/llm/agent/duckdb.py +++ /dev/null @@ -1,339 +0,0 @@ -from typing import Optional, Tuple - -from phi.llm.agent.base import BaseAgent -from phi.utils.log import logger - -try: - import duckdb -except ImportError: - raise ImportError("`duckdb` not installed. Please install it using `pip install duckdb`.") - - -class DuckDbAgent(BaseAgent): - def __init__( - self, - db_path: str = ":memory:", - s3_region: str = "us-east-1", - duckdb_connection: Optional[duckdb.DuckDBPyConnection] = None, - ): - super().__init__(name="duckdb_registry") - - self.db_path: str = db_path - self.s3_region: str = s3_region - self._duckdb_connection: Optional[duckdb.DuckDBPyConnection] = duckdb_connection - - self.register(self.run_duckdb_query) - self.register(self.show_tables) - self.register(self.describe_table) - self.register(self.inspect_query) - self.register(self.describe_table_or_view) - self.register(self.export_table_as) - self.register(self.summarize_table) - self.register(self.create_fts_index) - self.register(self.full_text_search) - - @property - def duckdb_connection(self) -> duckdb.DuckDBPyConnection: - """ - Returns the duckdb connection - - :return duckdb.DuckDBPyConnection: duckdb connection - """ - if self._duckdb_connection is None: - self._duckdb_connection = duckdb.connect(self.db_path) - try: - self._duckdb_connection.sql("INSTALL httpfs;") - self._duckdb_connection.sql("LOAD httpfs;") - self._duckdb_connection.sql(f"SET s3_region='{self.s3_region}';") - except Exception as e: - logger.exception(e) - logger.warning("Failed to install httpfs extension. Only local files will be supported") - - return self._duckdb_connection - - def run_duckdb_query(self, query: str) -> str: - """Function to run SQL queries against a duckdb database - - :param query: SQL query to run - :return: Result of the query - """ - - # -*- Format the SQL Query - # Remove backticks - formatted_sql = query.replace("`", "") - # If there are multiple statements, only run the first one - formatted_sql = formatted_sql.split(";")[0] - - try: - logger.debug(f"Running query: {formatted_sql}") - - query_result = self.duckdb_connection.sql(formatted_sql) - result_output = "No output" - if query_result is not None: - try: - results_as_python_objects = query_result.fetchall() - result_rows = [] - for row in results_as_python_objects: - if len(row) == 1: - result_rows.append(str(row[0])) - else: - result_rows.append(",".join(str(x) for x in row)) - - result_data = "\n".join(result_rows) - result_output = ",".join(query_result.columns) + "\n" + result_data - except AttributeError: - result_output = str(query_result) - - logger.debug(f"Query result: {result_output}") - return result_output - except duckdb.ProgrammingError as e: - return str(e) - except duckdb.Error as e: - return str(e) - except Exception as e: - return str(e) - - def show_tables(self) -> str: - """Function to show tables in the database - - :return: List of tables in the database - """ - stmt = "SHOW TABLES;" - tables = self.run_duckdb_query(stmt) - logger.debug(f"Tables: {tables}") - return tables - - def describe_table(self, table: str) -> str: - """Function to describe a table - - :param table: Table to describe - :return: Description of the table - """ - stmt = f"DESCRIBE {table};" - table_description = self.run_duckdb_query(stmt) - - logger.debug(f"Table description: {table_description}") - return f"{table}\n{table_description}" - - def summarize_table(self, table: str) -> str: - """Function to summarize the contents of a table - - :param table: Table to describe - :return: Description of the table - """ - stmt = f"SUMMARIZE SELECT * FROM {table};" - table_description = self.run_duckdb_query(stmt) - - logger.debug(f"Table description: {table_description}") - return f"{table}\n{table_description}" - - def inspect_query(self, query: str) -> str: - """Function to inspect a query and return the query plan. Always inspect your query before running them. - - :param query: Query to inspect - :return: Qeury plan - """ - stmt = f"explain {query};" - explain_plan = self.run_duckdb_query(stmt) - - logger.debug(f"Explain plan: {explain_plan}") - return explain_plan - - def describe_table_or_view(self, table: str): - """Function to describe a table or view - - :param table: Table or view to describe - :return: Description of the table or view - """ - stmt = f"select column_name, data_type from information_schema.columns where table_name='{table}';" - table_description = self.run_duckdb_query(stmt) - - logger.debug(f"Table description: {table_description}") - return f"{table}\n{table_description}" - - def load_local_path_to_table(self, path: str, table_name: Optional[str] = None) -> Tuple[str, str]: - """Load a local file into duckdb - - :param path: Path to load - :param table_name: Optional table name to use - :return: Table name, SQL statement used to load the file - """ - import os - - logger.debug(f"Loading {path} into duckdb") - - if table_name is None: - # Get the file name from the s3 path - file_name = path.split("/")[-1] - # Get the file name without extension from the s3 path - table_name, extension = os.path.splitext(file_name) - # If the table_name isn't a valid SQL identifier, we'll need to use something else - table_name = table_name.replace("-", "_").replace(".", "_").replace(" ", "_").replace("/", "_") - - create_statement = f"CREATE OR REPLACE TABLE '{table_name}' AS SELECT * FROM '{path}';" - self.run_duckdb_query(create_statement) - - logger.debug(f"Loaded {path} into duckdb as {table_name}") - # self.run_duckdb_query(f"SELECT * from {table_name};") - return table_name, create_statement - - def load_local_csv_to_table( - self, path: str, table_name: Optional[str] = None, delimiter: Optional[str] = None - ) -> Tuple[str, str]: - """Load a local CSV file into duckdb - - :param path: Path to load - :param table_name: Optional table name to use - :param delimiter: Optional delimiter to use - :return: Table name, SQL statement used to load the file - """ - import os - - logger.debug(f"Loading {path} into duckdb") - - if table_name is None: - # Get the file name from the s3 path - file_name = path.split("/")[-1] - # Get the file name without extension from the s3 path - table_name, extension = os.path.splitext(file_name) - # If the table_name isn't a valid SQL identifier, we'll need to use something else - table_name = table_name.replace("-", "_").replace(".", "_").replace(" ", "_").replace("/", "_") - - select_statement = f"SELECT * FROM read_csv('{path}'" - if delimiter is not None: - select_statement += f", delim='{delimiter}')" - else: - select_statement += ")" - - create_statement = f"CREATE OR REPLACE TABLE '{table_name}' AS {select_statement};" - self.run_duckdb_query(create_statement) - - logger.debug(f"Loaded CSV {path} into duckdb as {table_name}") - # self.run_duckdb_query(f"SELECT * from {table_name};") - return table_name, create_statement - - def load_s3_path_to_table(self, s3_path: str, table_name: Optional[str] = None) -> Tuple[str, str]: - """Load a file from S3 into duckdb - - :param s3_path: S3 path to load - :param table_name: Optional table name to use - :return: Table name, SQL statement used to load the file - """ - import os - - logger.debug(f"Loading {s3_path} into duckdb") - - if table_name is None: - # Get the file name from the s3 path - file_name = s3_path.split("/")[-1] - # Get the file name without extension from the s3 path - table_name, extension = os.path.splitext(file_name) - # If the table_name isn't a valid SQL identifier, we'll need to use something else - table_name = table_name.replace("-", "_").replace(".", "_").replace(" ", "_").replace("/", "_") - - create_statement = f"CREATE OR REPLACE TABLE '{table_name}' AS SELECT * FROM '{s3_path}';" - self.run_duckdb_query(create_statement) - - logger.debug(f"Loaded {s3_path} into duckdb as {table_name}") - # self.run_duckdb_query(f"SELECT * from {table_name};") - return table_name, create_statement - - def load_s3_csv_to_table( - self, s3_path: str, table_name: Optional[str] = None, delimiter: Optional[str] = None - ) -> Tuple[str, str]: - """Load a CSV file from S3 into duckdb - - :param s3_path: S3 path to load - :param table_name: Optional table name to use - :return: Table name, SQL statement used to load the file - """ - import os - - logger.debug(f"Loading {s3_path} into duckdb") - - if table_name is None: - # Get the file name from the s3 path - file_name = s3_path.split("/")[-1] - # Get the file name without extension from the s3 path - table_name, extension = os.path.splitext(file_name) - # If the table_name isn't a valid SQL identifier, we'll need to use something else - table_name = table_name.replace("-", "_").replace(".", "_").replace(" ", "_").replace("/", "_") - - select_statement = f"SELECT * FROM read_csv('{s3_path}'" - if delimiter is not None: - select_statement += f", delim='{delimiter}')" - else: - select_statement += ")" - - create_statement = f"CREATE OR REPLACE TABLE '{table_name}' AS {select_statement};" - self.run_duckdb_query(create_statement) - - logger.debug(f"Loaded CSV {s3_path} into duckdb as {table_name}") - # self.run_duckdb_query(f"SELECT * from {table_name};") - return table_name, create_statement - - def export_table_as(self, table_name: str, format: Optional[str] = "PARQUET", path: Optional[str] = None) -> str: - """Save a table to a desired format - The function will use the default format as parquet - If the path is provided, the table will be exported to that path, example s3 - - :param table_name: Table to export - :param format: Format to export to - :param path: Path to export to - :return: None - """ - if format is None: - format = "PARQUET" - - logger.debug(f"Exporting Table {table_name} as {format.upper()} in the path {path}") - # self.run_duckdb_query(f"SELECT * from {table_name};") - if path is None: - path = f"{table_name}.{format}" - else: - path = f"{path}/{table_name}.{format}" - export_statement = f"COPY (SELECT * FROM {table_name}) TO '{path}' (FORMAT {format.upper()});" - result = self.run_duckdb_query(export_statement) - logger.debug(f"Exported {table_name} to {path}/{table_name}") - - return result - - def create_fts_index(self, table_name: str, unique_key: str, input_values: list[str]) -> str: - """Create a full text search index on a table - - :param table_name: Table to create the index on - :param unique_key: Unique key to use - :param input_values: Values to index - :return: None - """ - logger.debug(f"Creating FTS index on {table_name} for {input_values}") - self.run_duckdb_query("INSTALL fts;") - logger.debug("Installed FTS extension") - self.run_duckdb_query("LOAD fts;") - logger.debug("Loaded FTS extension") - - create_fts_index_statement = f"PRAGMA create_fts_index('{table_name}', '{unique_key}', '{input_values}');" - logger.debug(f"Running {create_fts_index_statement}") - result = self.run_duckdb_query(create_fts_index_statement) - logger.debug(f"Created FTS index on {table_name} for {input_values}") - - return result - - def full_text_search(self, table_name: str, unique_key: str, search_text: str) -> str: - """Full text Search in a table column for a specific text/keyword - - :param table_name: Table to search - :param unique_key: Unique key to use - :param search_text: Text to search - :return: None - """ - logger.debug(f"Running full_text_search for {search_text} in {table_name}") - search_text_statement = f"""SELECT fts_main_corpus.match_bm25({unique_key}, '{search_text}') AS score,* - FROM {table_name} - WHERE score IS NOT NULL - ORDER BY score;""" - - logger.debug(f"Running {search_text_statement}") - result = self.run_duckdb_query(search_text_statement) - logger.debug(f"Search results for {search_text} in {table_name}") - - return result diff --git a/phi/llm/agent/email.py b/phi/llm/agent/email.py deleted file mode 100644 index 1df6a7201..000000000 --- a/phi/llm/agent/email.py +++ /dev/null @@ -1,59 +0,0 @@ -from typing import Optional - -from phi.llm.agent.base import BaseAgent -from phi.utils.log import logger - - -class EmailAgent(BaseAgent): - def __init__( - self, - receiver_email: Optional[str] = None, - sender_name: Optional[str] = None, - sender_email: Optional[str] = None, - sender_passkey: Optional[str] = None, - ): - super().__init__(name="email_agent") - self.receiver_email: Optional[str] = receiver_email - self.sender_name: Optional[str] = sender_name - self.sender_email: Optional[str] = sender_email - self.sender_passkey: Optional[str] = sender_passkey - self.register(self.email_user) - - def email_user(self, subject: str, body: str) -> str: - """Emails the user with the given subject and body. - - :param subject: The subject of the email. - :param body: The body of the email. - :return: "success" if the email was sent successfully, "error: [error message]" otherwise. - """ - try: - import smtplib - from email.message import EmailMessage - except ImportError: - logger.error("`smtplib` not installed") - raise - - if not self.receiver_email: - return "error: No receiver email provided" - if not self.sender_name: - return "error: No sender name provided" - if not self.sender_email: - return "error: No sender email provided" - if not self.sender_passkey: - return "error: No sender passkey provided" - - msg = EmailMessage() - msg["Subject"] = subject - msg["From"] = f"{self.sender_name} <{self.sender_email}>" - msg["To"] = self.receiver_email - msg.set_content(body) - - logger.info(f"Sending Email to {self.receiver_email}") - try: - with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp: - smtp.login(self.sender_email, self.sender_passkey) - smtp.send_message(msg) - except Exception as e: - logger.error(f"Error sending email: {e}") - return f"error: {e}" - return "email sent successfully" diff --git a/phi/llm/agent/google.py b/phi/llm/agent/google.py deleted file mode 100644 index 79f3ca7f3..000000000 --- a/phi/llm/agent/google.py +++ /dev/null @@ -1,18 +0,0 @@ -from phi.llm.agent.base import BaseAgent -from phi.utils.log import logger - - -class GoogleAgent(BaseAgent): - def __init__(self): - super().__init__(name="google_agent") - self.register(self.get_result_from_google) - - def get_result_from_google(self, query: str) -> str: - """Gets the result for a query from Google. - Use this function to find an answer when not available in the knowledge base. - - :param query: The query to search for. - :return: The result from Google. - """ - logger.info(f"Searching google for: {query}") - return "Sorry, this capability is not available yet." diff --git a/phi/llm/agent/phi.py b/phi/llm/agent/phi.py deleted file mode 100644 index 872e4d6ea..000000000 --- a/phi/llm/agent/phi.py +++ /dev/null @@ -1,116 +0,0 @@ -import uuid -from typing import Optional - -from phi.llm.agent.base import BaseAgent -from phi.utils.log import logger - - -class PhiAgent(BaseAgent): - def __init__(self): - super().__init__(name="phi_agent") - self.register(self.create_new_app) - self.register(self.start_user_workspace) - self.register(self.validate_phi_is_ready) - - def validate_phi_is_ready(self) -> bool: - """Validates that Phi is ready to run commands. - - :return: True if Phi is ready, False otherwise. - """ - # Check if docker is running - return True - - def create_new_app(self, template: str, workspace_name: str) -> str: - """Creates a new phidata workspace for a given application template. - Use this function when the user wants to create a new "llm-app", "api-app", "django-app", or "streamlit-app". - Remember to provide a name for the new workspace. - You can use the format: "template-name" + name of an interesting person (lowercase, no spaces). - - :param template: (required) The template to use for the new application. - One of: llm-app, api-app, django-app, streamlit-app - :param workspace_name: (required) The name of the workspace to create for the new application. - :return: Status of the function or next steps. - """ - from phi.workspace.operator import create_workspace, TEMPLATE_TO_NAME_MAP, WorkspaceStarterTemplate - - ws_template: Optional[WorkspaceStarterTemplate] = None - if template.lower() in WorkspaceStarterTemplate.__members__.values(): - ws_template = WorkspaceStarterTemplate(template) - - if ws_template is None: - return f"Error: Invalid template: {template}, must be one of: llm-app, api-app, django-app, streamlit-app" - - ws_dir_name: Optional[str] = workspace_name - if ws_dir_name is None: - # Get default_ws_name from template - default_ws_name: Optional[str] = TEMPLATE_TO_NAME_MAP.get(ws_template) - # Add a 2 digit random suffix to the default_ws_name - random_suffix = str(uuid.uuid4())[:2] - default_ws_name = f"{default_ws_name}-{random_suffix}" - - return ( - f"Ask the user for a name for the app directory with the default value: {default_ws_name}." - f"Ask the user to input YES or NO to use the default value." - ) - # # Ask user for workspace name if not provided - # ws_dir_name = Prompt.ask("Please provide a name for the app", default=default_ws_name, console=console) - - logger.info(f"Creating: {template} at {ws_dir_name}") - try: - create_successful = create_workspace(name=ws_dir_name, template=ws_template.value) - if create_successful: - return ( - f"Successfully created a {ws_template.value} at {ws_dir_name}. " - f"Ask the user if they want to start the app now." - ) - else: - return f"Error: Failed to create {template}" - except Exception as e: - return f"Error: {e}" - - def start_user_workspace(self, workspace_name: Optional[str] = None) -> str: - """Starts the workspace for a user. Use this function when the user wants to start a given workspace. - If the workspace name is not provided, the function will start the active workspace. - Otherwise, it will start the workspace with the given name. - - :param workspace_name: The name of the workspace to start - :return: Status of the function or next steps. - """ - from phi.cli.config import PhiCliConfig - from phi.infra.type import InfraType - from phi.workspace.config import WorkspaceConfig - from phi.workspace.operator import start_workspace - - phi_config: Optional[PhiCliConfig] = PhiCliConfig.from_saved_config() - if not phi_config: - return "Error: Phi not initialized. Please run `phi ai` again" - - workspace_config_to_start: Optional[WorkspaceConfig] = None - active_ws_config: Optional[WorkspaceConfig] = phi_config.get_active_ws_config() - - if workspace_name is None: - if active_ws_config is None: - return "Error: No active workspace found. Please create a workspace first." - workspace_config_to_start = active_ws_config - else: - workspace_config_by_name: Optional[WorkspaceConfig] = phi_config.get_ws_config_by_dir_name(workspace_name) - if workspace_config_by_name is None: - return f"Error: Could not find a workspace with name: {workspace_name}" - workspace_config_to_start = workspace_config_by_name - - # Set the active workspace to the workspace to start - if active_ws_config is not None and active_ws_config.ws_root_path != workspace_config_by_name.ws_root_path: - phi_config.set_active_ws_dir(workspace_config_by_name.ws_root_path) - active_ws_config = workspace_config_by_name - - try: - start_workspace( - phi_config=phi_config, - ws_config=workspace_config_to_start, - target_env="dev", - target_infra=InfraType.docker, - auto_confirm=True, - ) - return f"Successfully started workspace: {workspace_config_to_start.ws_root_path.stem}" - except Exception as e: - return f"Error: {e}" diff --git a/phi/llm/agent/pubmed.py b/phi/llm/agent/pubmed.py deleted file mode 100644 index 15c04e97b..000000000 --- a/phi/llm/agent/pubmed.py +++ /dev/null @@ -1,19 +0,0 @@ -from phi.llm.agent.base import BaseAgent -from phi.utils.log import logger - - -class PubMedAgent(BaseAgent): - def __init__(self): - super().__init__(name="pubmed_agent") - self.register(self.get_articles_from_pubmed) - - def get_articles_from_pubmed(self, query: str, num_articles: int = 2) -> str: - """Gets the abstract for articles related to a query from PubMed: a database of biomedical literature - Use this function to find information about a medical concept when not available in the knowledge base or Google - - :param query: The query to get related articles for. - :param num_articles: The number of articles to return. - :return: JSON string containing the articles - """ - logger.info(f"Searching Pubmed for: {query}") - return "Sorry, this capability is not available yet." diff --git a/phi/llm/agent/shell.py b/phi/llm/agent/shell.py deleted file mode 100644 index 4cb4dcc29..000000000 --- a/phi/llm/agent/shell.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import List - -from phi.llm.agent.base import BaseAgent -from phi.utils.log import logger - - -class ShellAgent(BaseAgent): - def __init__(self): - super().__init__(name="shell_agent") - self.register(self.run_shell_command) - - def run_shell_command(self, args: List[str], tail: int = 100) -> str: - """Runs a shell command and returns the output or error. - - :param args: The command to run as a list of strings. - :param tail: The number of lines to return from the output. - :return: The output of the command. - """ - logger.info(f"Running shell command: {args}") - - import subprocess - - try: - result = subprocess.run(args, capture_output=True, text=True) - logger.debug(f"Result: {result}") - logger.debug(f"Return code: {result.returncode}") - if result.returncode != 0: - return f"Error: {result.stderr}" - - # return only the last n lines of the output - return "\n".join(result.stdout.split("\n")[-tail:]) - except Exception as e: - logger.warning(f"Failed to run shell command: {e}") - return f"Error: {e}" diff --git a/phi/llm/agent/website.py b/phi/llm/agent/website.py deleted file mode 100644 index 5620b0393..000000000 --- a/phi/llm/agent/website.py +++ /dev/null @@ -1,50 +0,0 @@ -import json -from typing import List, Optional - -from phi.document import Document -from phi.knowledge.website import WebsiteKnowledgeBase -from phi.llm.agent.base import BaseAgent -from phi.utils.log import logger - - -class WebsiteAgent(BaseAgent): - def __init__(self, knowledge_base: Optional[WebsiteKnowledgeBase] = None): - super().__init__(name="website_agent") - self.knowledge_base: Optional[WebsiteKnowledgeBase] = knowledge_base - - if self.knowledge_base is not None and isinstance(self.knowledge_base, WebsiteKnowledgeBase): - self.register(self.add_website_to_knowledge_base) - else: - self.register(self.read_website) - - def add_website_to_knowledge_base(self, url: str) -> str: - """This function adds a websites content to the knowledge base. - NOTE: The website must start with https:// and should be a valid website. - - USE THIS FUNCTION TO GET INFORMATION ABOUT PRODUCTS FROM THE INTERNET. - - :param url: The url of the website to add. - :return: 'Success' if the website was added to the knowledge base. - """ - if self.knowledge_base is None: - return "Knowledge base not provided" - - logger.debug(f"Adding to knowledge base: {url}") - self.knowledge_base.urls.append(url) - logger.debug("Loading knowledge base.") - self.knowledge_base.load(recreate=False) - return "Success" - - def read_website(self, url: str) -> str: - """This function reads a website and returns the content. - - :param url: The url of the website to read. - :return: Relevant documents from the website. - """ - from phi.document.reader.website import WebsiteReader - - website = WebsiteReader() - - logger.debug(f"Reading website: {url}") - relevant_docs: List[Document] = website.read(url=url) - return json.dumps([doc.to_dict() for doc in relevant_docs]) diff --git a/phi/llm/agent/wikipedia.py b/phi/llm/agent/wikipedia.py deleted file mode 100644 index ff2bb2377..000000000 --- a/phi/llm/agent/wikipedia.py +++ /dev/null @@ -1,54 +0,0 @@ -import json -from typing import List, Optional - -from phi.document import Document -from phi.knowledge.wikipedia import WikipediaKnowledgeBase -from phi.llm.agent.base import BaseAgent -from phi.utils.log import logger - - -class WikipediaAgent(BaseAgent): - def __init__(self, knowledge_base: Optional[WikipediaKnowledgeBase] = None): - super().__init__(name="wikipedia_agent") - self.knowledge_base: Optional[WikipediaKnowledgeBase] = knowledge_base - - if self.knowledge_base is not None and isinstance(self.knowledge_base, WikipediaKnowledgeBase): - self.register(self.search_wikipedia_and_update_knowledge_base) - else: - self.register(self.search_wikipedia) - - def search_wikipedia_and_update_knowledge_base(self, topic: str) -> str: - """This function searches wikipedia for a topic, adds the results to the knowledge base and returns them. - - USE THIS FUNCTION TO GET INFORMATION WHICH DOES NOT EXIST. - - :param topic: The topic to search Wikipedia and add to knowledge base. - :return: Relevant documents from Wikipedia knowledge base. - """ - - if self.knowledge_base is None: - return "Knowledge base not provided" - - logger.debug(f"Adding to knowledge base: {topic}") - self.knowledge_base.topics.append(topic) - logger.debug("Loading knowledge base.") - self.knowledge_base.load(recreate=False) - logger.debug(f"Searching knowledge base: {topic}") - relevant_docs: List[Document] = self.knowledge_base.search(query=topic) - return json.dumps([doc.to_dict() for doc in relevant_docs]) - - def search_wikipedia(self, query: str) -> str: - """Searches Wikipedia for a query. - - :param query: The query to search for. - :return: Relevant documents from wikipedia. - """ - try: - import wikipedia # noqa: F401 - except ImportError: - raise ImportError( - "The `wikipedia` package is not installed. " "Please install it via `pip install wikipedia`." - ) - - logger.info(f"Searching wikipedia for: {query}") - return json.dumps(Document(name=query, content=wikipedia.summary(query)).to_dict()) diff --git a/phi/llm/base.py b/phi/llm/base.py index 28864edcd..a3d217263 100644 --- a/phi/llm/base.py +++ b/phi/llm/base.py @@ -1,27 +1,26 @@ -import json from typing import List, Iterator, Optional, Dict, Any, Callable, Union from pydantic import BaseModel, ConfigDict -from phi.llm.schemas import Message, Function, FunctionCall -from phi.llm.agent.base import BaseAgent -from phi.llm.tool.base import BaseTool -from phi.llm.function.registry import FunctionRegistry +from phi.llm.schemas import Message +from phi.tool.tool import Tool +from phi.tool.registry import ToolRegistry +from phi.tool.function import Function, FunctionCall from phi.utils.log import logger class LLM(BaseModel): # ID of the model to use. model: str - # Name for this LLM (not used for api calls). + # Name for this LLM. Note: This is not sent to the LLM API. name: Optional[str] = None - # Metrics collected for this LLM (not used for api calls). + # Metrics collected for this LLM. Note: This is not sent to the LLM API. metrics: Dict[str, Any] = {} # A list of tools the model may call. # Currently, only functions are supported as a tool. - # Use this to provide a list of functions the model may generate JSON inputs for. - tools: Optional[List[BaseTool]] = None + # Always add tools using the add_tool() method. + tools: Optional[List[Union[Tool, Dict]]] = None # Controls which (if any) function is called by the model. # "none" means the model will not call a function and instead generates a message. # "auto" means the model can pick between generating a message or calling a function. @@ -29,20 +28,18 @@ class LLM(BaseModel): # forces the model to call that function. # "none" is the default when no functions are present. "auto" is the default if functions are present. tool_choice: Optional[Union[str, Dict[str, Any]]] = None - # A list of functions to add to the tools - functions: Optional[Dict[str, Function]] = None + # -*- Functions available to the LLM to call -*- + # Functions provided from the tools. Note: These are not sent to the LLM API. + functions: Optional[Dict[str, Function]] = None + # If True, runs function calls before sending back the response content. + run_function_calls: bool = True + # If True, shows function calls in the response. + show_function_calls: Optional[bool] = None # Maximum number of function calls allowed. - function_call_limit: int = 50 + function_call_limit: int = 100 # Stack of function calls. function_call_stack: Optional[List[FunctionCall]] = None - # If True, shows function calls in the response. - show_function_calls: Optional[bool] = None - # If True, runs function calls before sending back the response content. - run_function_calls: bool = True - - # NOTE: Deprecated in favor of tool_choice. - function_call: Optional[str] = None model_config = ConfigDict(arbitrary_types_allowed=True) @@ -75,86 +72,39 @@ def to_dict(self) -> Dict[str, Any]: _dict["function_call_limit"] = self.function_call_limit return _dict - def add_function(self, f: Callable) -> None: - func = Function.from_callable(f) - if self.functions is None: - self.functions = {} - self.functions[func.name] = func - logger.debug(f"Added function {func.name} to LLM.") - - def add_function_schema(self, func: Function, if_not_exists: bool = True) -> None: - if self.functions is None: - self.functions = {} - - if if_not_exists and func.name in self.functions: - return - - self.functions[func.name] = func - logger.debug(f"Added function {func.name} to LLM.") - - def add_function_registry(self, registry: FunctionRegistry) -> None: - if self.functions is None: - self.functions = {} - - self.functions.update(registry.functions) - logger.debug(f"Functions from {registry.name} added to LLM.") - - def add_agent(self, agent: BaseAgent) -> None: - if self.functions is None: - self.functions = {} - - self.functions.update(agent.functions) - logger.debug(f"Agent: {agent.name} added to LLM.") - - def get_function_call(self, name: str, arguments: Optional[str] = None) -> Optional[FunctionCall]: - logger.debug(f"Getting function {name}. Args: {arguments}") - if self.functions is None: - return None - - function_to_call: Optional[Function] = None - if name in self.functions: - function_to_call = self.functions[name] - if function_to_call is None: - logger.error(f"Function {name} not found") + def get_tools_for_api(self) -> Optional[List[Dict[str, Any]]]: + if self.tools is None: return None - function_call = FunctionCall(function=function_to_call) - if arguments is not None and arguments != "": - try: - if "None" in arguments: - arguments = arguments.replace("None", "null") - if "True" in arguments: - arguments = arguments.replace("True", "true") - if "False" in arguments: - arguments = arguments.replace("False", "false") - _arguments = json.loads(arguments) - except Exception as e: - logger.error(f"Unable to decode function arguments {arguments}: {e}") - return None - - if not isinstance(_arguments, dict): - logger.error(f"Function arguments {arguments} is not a valid JSON object") - return None - - try: - clean_arguments: Dict[str, Any] = {} - for k, v in _arguments.items(): - if isinstance(v, str): - _v = v.strip().lower() - if _v in ("none", "null"): - clean_arguments[k] = None - elif _v == "true": - clean_arguments[k] = True - elif _v == "false": - clean_arguments[k] = False - else: - clean_arguments[k] = v.strip() - else: - clean_arguments[k] = v - - function_call.arguments = clean_arguments - except Exception as e: - logger.error(f"Unable to parse function arguments {arguments}: {e}") - return None - - return function_call + tools_for_api = [] + for tool in self.tools: + if isinstance(tool, Tool): + tools_for_api.append(tool.to_dict()) + elif isinstance(tool, Dict): + tools_for_api.append(tool) + return tools_for_api + + def add_tool(self, tool: Union[Tool, Dict, Callable, ToolRegistry]) -> None: + if self.tools is None: + self.tools = [] + + # If the tool is a Tool or Dict, add it directly to the LLM + if isinstance(tool, Tool) or isinstance(tool, Dict): + self.tools.append(tool) + logger.debug(f"Added tool {tool} to LLM.") + + # If the tool is a Callable or ToolRegistry, add its functions to the LLM + if callable(tool) or isinstance(tool, ToolRegistry): + if self.functions is None: + self.functions = {} + + if isinstance(tool, ToolRegistry): + self.functions.update(tool.functions) + for func in tool.functions.values(): + self.tools.append({"type": "function", "function": func.to_dict()}) + logger.debug(f"Functions from {tool.name} added to LLM.") + elif callable(tool): + func = Function.from_callable(tool) + self.functions[func.name] = func + self.tools.append({"type": "function", "function": func.to_dict()}) + logger.debug(f"Added function {func.name} to LLM.") diff --git a/phi/llm/function/__init__.py b/phi/llm/function/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/phi/llm/function/arxiv.py b/phi/llm/function/arxiv.py deleted file mode 100644 index a540f4e64..000000000 --- a/phi/llm/function/arxiv.py +++ /dev/null @@ -1,53 +0,0 @@ -import json -from typing import List, Optional - -from phi.document import Document -from phi.knowledge.arxiv import ArxivKnowledgeBase -from phi.llm.function.registry import FunctionRegistry -from phi.utils.log import logger - - -class ArxivRegistry(FunctionRegistry): - def __init__(self, knowledge_base: Optional[ArxivKnowledgeBase] = None): - super().__init__(name="arxiv_registry") - self.knowledge_base: Optional[ArxivKnowledgeBase] = knowledge_base - - if self.knowledge_base is not None and isinstance(self.knowledge_base, ArxivKnowledgeBase): - self.register(self.search_arxiv_and_update_knowledge_base) - else: - self.register(self.search_arxiv) - - def search_arxiv_and_update_knowledge_base(self, topic: str) -> str: - """This function searches arXiv for a topic, adds the results to the knowledge base and returns them. - - USE THIS FUNCTION TO GET INFORMATION WHICH DOES NOT EXIST. - - :param topic: The topic to search arXiv and add to knowledge base. - :return: Relevant documents from arXiv knowledge base. - """ - if self.knowledge_base is None: - return "Knowledge base not provided" - - logger.debug(f"Adding to knowledge base: {topic}") - self.knowledge_base.queries.append(topic) - logger.debug("Loading knowledge base.") - self.knowledge_base.load(recreate=False) - logger.debug(f"Searching knowledge base: {topic}") - relevant_docs: List[Document] = self.knowledge_base.search(query=topic) - return json.dumps([doc.to_dict() for doc in relevant_docs]) - - def search_arxiv(self, query: str, max_results: int = 5) -> str: - """ - Searches arXiv for a query. - - :param query: The query to search for. - :param max_results: The maximum number of results to return. - :return: Relevant documents from arXiv. - """ - from phi.document.reader.arxiv import ArxivReader - - arxiv = ArxivReader(max_results=max_results) - - logger.debug(f"Searching arxiv for: {query}") - relevant_docs: List[Document] = arxiv.read(query=query) - return json.dumps([doc.to_dict() for doc in relevant_docs]) diff --git a/phi/llm/function/email.py b/phi/llm/function/email.py deleted file mode 100644 index 42d8d2b68..000000000 --- a/phi/llm/function/email.py +++ /dev/null @@ -1,59 +0,0 @@ -from typing import Optional - -from phi.llm.function.registry import FunctionRegistry -from phi.utils.log import logger - - -class EmailRegistry(FunctionRegistry): - def __init__( - self, - receiver_email: Optional[str] = None, - sender_name: Optional[str] = None, - sender_email: Optional[str] = None, - sender_passkey: Optional[str] = None, - ): - super().__init__(name="email_registry") - self.receiver_email: Optional[str] = receiver_email - self.sender_name: Optional[str] = sender_name - self.sender_email: Optional[str] = sender_email - self.sender_passkey: Optional[str] = sender_passkey - self.register(self.email_user) - - def email_user(self, subject: str, body: str) -> str: - """Emails the user with the given subject and body. - - :param subject: The subject of the email. - :param body: The body of the email. - :return: "success" if the email was sent successfully, "error: [error message]" otherwise. - """ - try: - import smtplib - from email.message import EmailMessage - except ImportError: - logger.error("`smtplib` not installed") - raise - - if not self.receiver_email: - return "error: No receiver email provided" - if not self.sender_name: - return "error: No sender name provided" - if not self.sender_email: - return "error: No sender email provided" - if not self.sender_passkey: - return "error: No sender passkey provided" - - msg = EmailMessage() - msg["Subject"] = subject - msg["From"] = f"{self.sender_name} <{self.sender_email}>" - msg["To"] = self.receiver_email - msg.set_content(body) - - logger.info(f"Sending Email to {self.receiver_email}") - try: - with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp: - smtp.login(self.sender_email, self.sender_passkey) - smtp.send_message(msg) - except Exception as e: - logger.error(f"Error sending email: {e}") - return f"error: {e}" - return "email sent successfully" diff --git a/phi/llm/function/google.py b/phi/llm/function/google.py deleted file mode 100644 index f66e6a996..000000000 --- a/phi/llm/function/google.py +++ /dev/null @@ -1,18 +0,0 @@ -from phi.llm.function.registry import FunctionRegistry -from phi.utils.log import logger - - -class GoogleRegistry(FunctionRegistry): - def __init__(self): - super().__init__(name="google_registry") - self.register(self.get_result_from_google) - - def get_result_from_google(self, query: str) -> str: - """Gets the result for a query from Google. - Use this function to find an answer when not available in the knowledge base. - - :param query: The query to search for. - :return: The result from Google. - """ - logger.info(f"Searching google for: {query}") - return "Sorry, this capability is not available yet." diff --git a/phi/llm/function/phi_commands.py b/phi/llm/function/phi_commands.py deleted file mode 100644 index ff2c9ec3f..000000000 --- a/phi/llm/function/phi_commands.py +++ /dev/null @@ -1,116 +0,0 @@ -import uuid -from typing import Optional - -from phi.llm.function.registry import FunctionRegistry -from phi.utils.log import logger - - -class PhiCommandsRegistry(FunctionRegistry): - def __init__(self): - super().__init__(name="phi_commands_registry") - self.register(self.create_new_app) - self.register(self.start_user_workspace) - self.register(self.validate_phi_is_ready) - - def validate_phi_is_ready(self) -> bool: - """Validates that Phi is ready to run commands. - - :return: True if Phi is ready, False otherwise. - """ - # Check if docker is running - return True - - def create_new_app(self, template: str, workspace_name: str) -> str: - """Creates a new phidata workspace for a given application template. - Use this function when the user wants to create a new "llm-app", "api-app", "django-app", or "streamlit-app". - Remember to provide a name for the new workspace. - You can use the format: "template-name" + name of an interesting person (lowercase, no spaces). - - :param template: (required) The template to use for the new application. - One of: llm-app, api-app, django-app, streamlit-app - :param workspace_name: (required) The name of the workspace to create for the new application. - :return: Status of the function or next steps. - """ - from phi.workspace.operator import create_workspace, TEMPLATE_TO_NAME_MAP, WorkspaceStarterTemplate - - ws_template: Optional[WorkspaceStarterTemplate] = None - if template.lower() in WorkspaceStarterTemplate.__members__.values(): - ws_template = WorkspaceStarterTemplate(template) - - if ws_template is None: - return f"Error: Invalid template: {template}, must be one of: llm-app, api-app, django-app, streamlit-app" - - ws_dir_name: Optional[str] = workspace_name - if ws_dir_name is None: - # Get default_ws_name from template - default_ws_name: Optional[str] = TEMPLATE_TO_NAME_MAP.get(ws_template) - # Add a 2 digit random suffix to the default_ws_name - random_suffix = str(uuid.uuid4())[:2] - default_ws_name = f"{default_ws_name}-{random_suffix}" - - return ( - f"Ask the user for a name for the app directory with the default value: {default_ws_name}." - f"Ask the user to input YES or NO to use the default value." - ) - # # Ask user for workspace name if not provided - # ws_dir_name = Prompt.ask("Please provide a name for the app", default=default_ws_name, console=console) - - logger.info(f"Creating: {template} at {ws_dir_name}") - try: - create_successful = create_workspace(name=ws_dir_name, template=ws_template.value) - if create_successful: - return ( - f"Successfully created a {ws_template.value} at {ws_dir_name}. " - f"Ask the user if they want to start the app now." - ) - else: - return f"Error: Failed to create {template}" - except Exception as e: - return f"Error: {e}" - - def start_user_workspace(self, workspace_name: Optional[str] = None) -> str: - """Starts the workspace for a user. Use this function when the user wants to start a given workspace. - If the workspace name is not provided, the function will start the active workspace. - Otherwise, it will start the workspace with the given name. - - :param workspace_name: The name of the workspace to start - :return: Status of the function or next steps. - """ - from phi.cli.config import PhiCliConfig - from phi.infra.type import InfraType - from phi.workspace.config import WorkspaceConfig - from phi.workspace.operator import start_workspace - - phi_config: Optional[PhiCliConfig] = PhiCliConfig.from_saved_config() - if not phi_config: - return "Error: Phi not initialized. Please run `phi ai` again" - - workspace_config_to_start: Optional[WorkspaceConfig] = None - active_ws_config: Optional[WorkspaceConfig] = phi_config.get_active_ws_config() - - if workspace_name is None: - if active_ws_config is None: - return "Error: No active workspace found. Please create a workspace first." - workspace_config_to_start = active_ws_config - else: - workspace_config_by_name: Optional[WorkspaceConfig] = phi_config.get_ws_config_by_dir_name(workspace_name) - if workspace_config_by_name is None: - return f"Error: Could not find a workspace with name: {workspace_name}" - workspace_config_to_start = workspace_config_by_name - - # Set the active workspace to the workspace to start - if active_ws_config is not None and active_ws_config.ws_root_path != workspace_config_by_name.ws_root_path: - phi_config.set_active_ws_dir(workspace_config_by_name.ws_root_path) - active_ws_config = workspace_config_by_name - - try: - start_workspace( - phi_config=phi_config, - ws_config=workspace_config_to_start, - target_env="dev", - target_infra=InfraType.docker, - auto_confirm=True, - ) - return f"Successfully started workspace: {workspace_config_to_start.ws_root_path.stem}" - except Exception as e: - return f"Error: {e}" diff --git a/phi/llm/function/pubmed.py b/phi/llm/function/pubmed.py deleted file mode 100644 index a28bbc8c9..000000000 --- a/phi/llm/function/pubmed.py +++ /dev/null @@ -1,19 +0,0 @@ -from phi.llm.function.registry import FunctionRegistry -from phi.utils.log import logger - - -class PubMedRegistry(FunctionRegistry): - def __init__(self): - super().__init__(name="pubmed_registry") - self.register(self.get_articles_from_pubmed) - - def get_articles_from_pubmed(self, query: str, num_articles: int = 2) -> str: - """Gets the abstract for articles related to a query from PubMed: a database of biomedical literature - Use this function to find information about a medical concept when not available in the knowledge base or Google - - :param query: The query to get related articles for. - :param num_articles: The number of articles to return. - :return: JSON string containing the articles - """ - logger.info(f"Searching Pubmed for: {query}") - return "Sorry, this capability is not available yet." diff --git a/phi/llm/function/registry.py b/phi/llm/function/registry.py deleted file mode 100644 index 7b2dac4f6..000000000 --- a/phi/llm/function/registry.py +++ /dev/null @@ -1,27 +0,0 @@ -from collections import OrderedDict -from typing import Callable, Dict - -from phi.llm.schemas import Function -from phi.utils.log import logger - - -class FunctionRegistry: - def __init__(self, name: str = "default_registry"): - self.name: str = name - self.functions: Dict[str, Function] = OrderedDict() - - def register(self, function: Callable): - try: - f = Function.from_callable(function) - self.functions[f.name] = f - logger.debug(f"Function: {f.name} registered with {self.name}") - logger.debug(f"Json Schema: {f.to_dict()}") - except Exception as e: - logger.warning(f"Failed to create Function for: {function.__name__}") - raise e - - def __repr__(self): - return f"<{self.__class__.__name__} name={self.name} functions={list(self.functions.keys())}>" - - def __str__(self): - return self.__repr__() diff --git a/phi/llm/function/shell.py b/phi/llm/function/shell.py deleted file mode 100644 index 9f1a42824..000000000 --- a/phi/llm/function/shell.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import List - -from phi.llm.function.registry import FunctionRegistry -from phi.utils.log import logger - - -class ShellScriptsRegistry(FunctionRegistry): - def __init__(self): - super().__init__(name="shell_script_registry") - self.register(self.run_shell_command) - - def run_shell_command(self, args: List[str], tail: int = 100) -> str: - """Runs a shell command and returns the output or error. - - :param args: The command to run as a list of strings. - :param tail: The number of lines to return from the output. - :return: The output of the command. - """ - logger.info(f"Running shell command: {args}") - - import subprocess - - try: - result = subprocess.run(args, capture_output=True, text=True) - logger.debug(f"Result: {result}") - logger.debug(f"Return code: {result.returncode}") - if result.returncode != 0: - return f"Error: {result.stderr}" - - # return only the last n lines of the output - return "\n".join(result.stdout.split("\n")[-tail:]) - except Exception as e: - logger.warning(f"Failed to run shell command: {e}") - return f"Error: {e}" diff --git a/phi/llm/function/website.py b/phi/llm/function/website.py deleted file mode 100644 index 91a3963c8..000000000 --- a/phi/llm/function/website.py +++ /dev/null @@ -1,50 +0,0 @@ -import json -from typing import List, Optional - -from phi.document import Document -from phi.knowledge.website import WebsiteKnowledgeBase -from phi.llm.function.registry import FunctionRegistry -from phi.utils.log import logger - - -class WebsiteRegistry(FunctionRegistry): - def __init__(self, knowledge_base: Optional[WebsiteKnowledgeBase] = None): - super().__init__(name="website_registry") - self.knowledge_base: Optional[WebsiteKnowledgeBase] = knowledge_base - - if self.knowledge_base is not None and isinstance(self.knowledge_base, WebsiteKnowledgeBase): - self.register(self.add_website_to_knowledge_base) - else: - self.register(self.read_website) - - def add_website_to_knowledge_base(self, url: str) -> str: - """This function adds a websites content to the knowledge base. - NOTE: The website must start wit http:// or https:// and should be a valid website. - - USE THIS FUNCTION TO GET INFORMATION ABOUT PRODUCTS FROM THE INTERNET. - - :param url: The url of the website to add. - :return: 'Success' if the website was added to the knowledge base. - """ - if self.knowledge_base is None: - return "Knowledge base not provided" - - logger.debug(f"Adding to knowledge base: {url}") - self.knowledge_base.urls.append(url) - logger.debug("Loading knowledge base.") - self.knowledge_base.load(recreate=False) - return "Success" - - def read_website(self, url: str) -> str: - """This function reads a website and returns the content. - - :param url: The url of the website to read. - :return: Relevant documents from the website. - """ - from phi.document.reader.website import WebsiteReader - - website = WebsiteReader() - - logger.debug(f"Reading website: {url}") - relevant_docs: List[Document] = website.read(url=url) - return json.dumps([doc.to_dict() for doc in relevant_docs]) diff --git a/phi/llm/function/wikipedia.py b/phi/llm/function/wikipedia.py deleted file mode 100644 index b2ca66957..000000000 --- a/phi/llm/function/wikipedia.py +++ /dev/null @@ -1,54 +0,0 @@ -import json -from typing import List, Optional - -from phi.document import Document -from phi.knowledge.wikipedia import WikipediaKnowledgeBase -from phi.llm.function.registry import FunctionRegistry -from phi.utils.log import logger - - -class WikipediaRegistry(FunctionRegistry): - def __init__(self, knowledge_base: Optional[WikipediaKnowledgeBase] = None): - super().__init__(name="wikipedia_registry") - self.knowledge_base: Optional[WikipediaKnowledgeBase] = knowledge_base - - if self.knowledge_base is not None and isinstance(self.knowledge_base, WikipediaKnowledgeBase): - self.register(self.search_wikipedia_and_update_knowledge_base) - else: - self.register(self.search_wikipedia) - - def search_wikipedia_and_update_knowledge_base(self, topic: str) -> str: - """This function searches wikipedia for a topic, adds the results to the knowledge base and returns them. - - USE THIS FUNCTION TO GET INFORMATION WHICH DOES NOT EXIST. - - :param topic: The topic to search Wikipedia and add to knowledge base. - :return: Relevant documents from Wikipedia knowledge base. - """ - - if self.knowledge_base is None: - return "Knowledge base not provided" - - logger.debug(f"Adding to knowledge base: {topic}") - self.knowledge_base.topics.append(topic) - logger.debug("Loading knowledge base.") - self.knowledge_base.load(recreate=False) - logger.debug(f"Searching knowledge base: {topic}") - relevant_docs: List[Document] = self.knowledge_base.search(query=topic) - return json.dumps([doc.to_dict() for doc in relevant_docs]) - - def search_wikipedia(self, query: str) -> str: - """Searches Wikipedia for a query. - - :param query: The query to search for. - :return: Relevant documents from wikipedia. - """ - try: - import wikipedia # noqa: F401 - except ImportError: - raise ImportError( - "The `wikipedia` package is not installed. " "Please install it via `pip install wikipedia`." - ) - - logger.info(f"Searching wikipedia for: {query}") - return json.dumps(Document(name=query, content=wikipedia.summary(query)).to_dict()) diff --git a/phi/llm/openai/chat.py b/phi/llm/openai/chat.py index a774dfddd..f91e226d3 100644 --- a/phi/llm/openai/chat.py +++ b/phi/llm/openai/chat.py @@ -1,10 +1,12 @@ from typing import Optional, List, Iterator, Dict, Any, Union, Tuple from phi.llm.base import LLM -from phi.llm.schemas import Message, FunctionCall +from phi.llm.schemas import Message +from phi.tool.function import FunctionCall from phi.utils.env import get_from_env from phi.utils.log import logger from phi.utils.timer import Timer +from phi.utils.functions import get_function_call try: from openai import OpenAI @@ -72,19 +74,10 @@ def api_kwargs(self) -> Dict[str, Any]: if self.headers: kwargs["headers"] = self.headers if self.tools: - kwargs["tools"] = [t.to_dict() for t in self.tools] - if self.tool_choice is not None: - kwargs["tool_choice"] = self.tool_choice - if self.functions: - if kwargs.get("tools") is None: - kwargs["tools"] = [] - for f in self.functions.values(): - function_tool = { - "type": "function", - "function": f.to_dict(), - } - kwargs["tools"].append(function_tool) - if self.tool_choice is not None: + kwargs["tools"] = self.get_tools_for_api() + if self.tool_choice is None: + kwargs["tool_choice"] = "auto" + else: kwargs["tool_choice"] = self.tool_choice return kwargs @@ -163,9 +156,15 @@ def invoke_model_stream(self, messages: List[Message]) -> Iterator[ChatCompletio # logger.debug(f"Double chunk: {chunk}") chunks = "[" + chunk.replace("}{", "},{") + "]" for completion_chunk in json.loads(chunks): - yield ChatCompletionChunk.model_validate_json(completion_chunk) + try: + yield ChatCompletionChunk.model_validate_json(completion_chunk) + except Exception as e: + logger.warning(e) else: - yield ChatCompletionChunk.model_validate_json(chunk) + try: + yield ChatCompletionChunk.model_validate_json(chunk) + except Exception as e: + logger.warning(e) except Exception as e: logger.exception(e) logger.info("Please message us on https://discord.gg/4MtYHHrgA8 for help.") @@ -183,7 +182,9 @@ def run_function(self, function_call: Dict[str, Any]) -> Tuple[Message, Optional _function_arguments_str = function_call.get("arguments") if _function_name is not None: # Get function call - _function_call = self.get_function_call(name=_function_name, arguments=_function_arguments_str) + _function_call = get_function_call( + name=_function_name, arguments=_function_arguments_str, functions=self.functions + ) if _function_call is None: return Message(role="function", content="Could not find function to call."), None @@ -200,7 +201,7 @@ def run_function(self, function_call: Dict[str, Any]) -> Tuple[Message, Optional self.function_call_stack.append(_function_call) _function_call_timer = Timer() _function_call_timer.start() - _function_call.run() + _function_call.execute() _function_call_timer.stop() _function_call_message = Message( role="function", @@ -227,8 +228,10 @@ def run_tool_calls(self, tool_calls: List[Dict[str, Any]]) -> List[Tuple[Message _tool_call_function_arguments_str = _tool_call_function.get("arguments") if _tool_call_function_name is not None: # Get tool call - function_call = self.get_function_call( - name=_tool_call_function_name, arguments=_tool_call_function_arguments_str + function_call = get_function_call( + name=_tool_call_function_name, + arguments=_tool_call_function_arguments_str, + functions=self.functions, ) if function_call is None: tool_call_results.append( @@ -256,7 +259,7 @@ def run_tool_calls(self, tool_calls: List[Dict[str, Any]]) -> List[Tuple[Message self.function_call_stack.append(function_call) tool_call_timer = Timer() tool_call_timer.start() - function_call.run() + function_call.execute() tool_call_timer.stop() tool_call_message = Message( role="tool", @@ -338,10 +341,6 @@ def parsed_response(self, messages: List[Message]) -> str: messages.append(assistant_message) assistant_message.log() - # -*- Return content if present, otherwise run function call - if assistant_message.content is not None: - return assistant_message.get_content_string() - # -*- Parse and run function call need_to_run_functions = assistant_message.function_call is not None or assistant_message.tool_calls is not None if need_to_run_functions and self.run_function_calls: @@ -364,7 +363,11 @@ def parsed_response(self, messages: List[Message]) -> str: # -*- Get new response using result of tool call final_response += self.parsed_response(messages=messages) return final_response + logger.debug("---------- OpenAI 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_message(self, messages: List[Message]) -> Dict: diff --git a/phi/llm/schemas.py b/phi/llm/schemas.py index 2f1d76e6b..014d7920c 100644 --- a/phi/llm/schemas.py +++ b/phi/llm/schemas.py @@ -1,5 +1,5 @@ -from typing import Optional, Any, Dict, Callable, List, get_type_hints, Union -from pydantic import BaseModel, validate_call +from typing import Optional, Any, Dict, List, Union +from pydantic import BaseModel from phi.utils.log import logger @@ -74,90 +74,3 @@ class References(BaseModel): references: str # Performance in seconds. time: Optional[float] = None - - -class Function(BaseModel): - """Model for LLM functions""" - - # The name of the function to be called. - # Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. - name: str - # A description of what the function does, used by the model to choose when and how to call the function. - description: Optional[str] = None - # The parameters the functions accepts, described as a JSON Schema object. - # To describe a function that accepts no parameters, provide the value {"type": "object", "properties": {}}. - parameters: Dict[str, Any] = {"type": "object", "properties": {}} - entrypoint: Optional[Callable] = None - - def to_dict(self) -> Dict[str, Any]: - return self.model_dump(exclude_none=True, exclude={"entrypoint"}) - - @classmethod - def from_callable(cls, c: Callable) -> "Function": - from inspect import getdoc - from phi.utils.json_schema import get_json_schema - - parameters = {"type": "object", "properties": {}} - try: - type_hints = get_type_hints(c) - parameters = get_json_schema(type_hints) - # logger.debug(f"Type hints for {c.__name__}: {type_hints}") - except Exception as e: - logger.warning(f"Could not parse args for {c.__name__}: {e}") - - return cls( - name=c.__name__, - description=getdoc(c), - parameters=parameters, - entrypoint=validate_call(c), - ) - - -class FunctionCall(BaseModel): - """Model for LLM function calls""" - - # The function to be called. - function: Function - # The arguments to call the function with. - arguments: Optional[Dict[str, Any]] = None - # The result of the function call. - result: Optional[Any] = None - - def get_call_str(self) -> str: - """Returns a string representation of the function call.""" - if self.arguments is None: - return f"{self.function.name}()" - return f"{self.function.name}({', '.join([f'{k}={v}' for k, v in self.arguments.items()])})" - - def run(self) -> bool: - """Runs the function call. - - @return: True if the function call was successful, False otherwise. - """ - if self.function.entrypoint is None: - return False - - logger.debug(f"Running: {self.get_call_str()}") - - # Call the function with no arguments if none are provided. - if self.arguments is None: - try: - self.result = self.function.entrypoint() - return True - except Exception as e: - logger.warning(f"Could not run function {self.get_call_str()}") - logger.error(e) - return False - - # Validate the arguments if provided. - # try: - # from jsonschema import validate - # except ImportError: - # raise ImportError("`jsonschema` is required for LLM functions, install using `pip install jsonschema`") - try: - self.result = self.function.entrypoint(**self.arguments) - return True - except Exception as e: - logger.warning(f"Could not run function {self.get_call_str()}") - logger.error(e) - return False diff --git a/phi/llm/task/llm_task.py b/phi/llm/task/llm_task.py index ad1998b55..e0e895474 100644 --- a/phi/llm/task/llm_task.py +++ b/phi/llm/task/llm_task.py @@ -6,10 +6,10 @@ from phi.document import Document from phi.knowledge.base import KnowledgeBase from phi.llm.base import LLM -from phi.llm.schemas import Message, References from phi.llm.openai import OpenAIChat -from phi.llm.agent.base import BaseAgent -from phi.llm.function.registry import FunctionRegistry +from phi.llm.schemas import Message, References +from phi.tool.tool import Tool +from phi.tool.registry import ToolRegistry from phi.llm.task.memory.base import TaskMemory from phi.utils.format_str import remove_indent from phi.utils.log import logger @@ -42,28 +42,32 @@ class LLMTask(BaseModel): add_references_to_prompt: bool = False # -*- Enable Function Calls - # Makes the task autonomous. + # Makes the task Autonomous by letting the LLM call functions to achieve tasks. function_calls: bool = False - # Add a list of default functions to the LLM + # Add default functions to the LLM when function_calls is True. default_functions: bool = True # Show function calls in LLM messages. show_function_calls: bool = False - # A list of functions to add to the LLM. - functions: Optional[List[Callable]] = None - # A list of function registries to add to the LLM. - function_registries: Optional[List[FunctionRegistry]] = None - # -*- Agents - # Add a list of agents to the LLM - # function_calls must be True for agents to be added to the LLM - agents: Optional[List[BaseAgent]] = None + # -*- Task Tools + # A list of tools provided to the LLM. + # Currently, only functions are supported as a tool. + # Use this to provide a list of functions the model may generate JSON inputs for. + tools: Optional[List[Union[Tool, Dict, Callable, ToolRegistry]]] = None + # Controls which (if any) function is called by the model. + # "none" means the model will not call a function and instead generates a message. + # "auto" means the model can pick between generating a message or calling a function. + # Specifying a particular function via {"type: "function", "function": {"name": "my_function"}} + # forces the model to call that function. + # "none" is the default when no functions are present. "auto" is the default if functions are present. + tool_choice: Optional[Union[str, Dict[str, Any]]] = None # # -*- Prompt Settings # - # -*- System prompt: provide the system prompt as a string or using a function + # -*- System prompt: provide the system prompt as a string system_prompt: Optional[str] = None - # Function to build the system prompt. + # -*- System prompt function: provide the system prompt as a function # This function is provided the task as an argument # and should return the system_prompt as a string. # Signature: @@ -73,10 +77,10 @@ class LLMTask(BaseModel): # If True, the task provides a default system prompt use_default_system_prompt: bool = True - # -*- User prompt: provide the user prompt as a string or using a function + # -*- User prompt: provide the user prompt as a string # Note: this will ignore the message provided to the run function user_prompt: Optional[Union[List[Dict], str]] = None - # Function to build the user prompt. + # -*- User prompt function: provide the user prompt as a function. # This function is provided the task and the input message as arguments # and should return the user_prompt as a Union[List[Dict], str]. # If add_references_to_prompt is True, then references are also provided as an argument. @@ -106,52 +110,25 @@ def set_task_id(self) -> None: if self.id is None: self.id = str(uuid4()) - def add_functions_to_llm(self) -> None: - if self.llm is None: - return - - if self.function_calls: - if self.default_functions: - default_func_list: List[Callable] = [ - self.get_last_n_chats, - self.search_knowledge_base, - ] - for func in default_func_list: - self.llm.add_function(func) - - # Add functions from self.functions - if self.functions is not None: - for func in self.functions: - self.llm.add_function(func) - - # Add functions from registries - if self.function_registries is not None: - for registry in self.function_registries: - self.llm.add_function_registry(registry) - - # Set function call to auto if it is not set - if self.llm.function_call is None: - self.llm.function_call = "auto" - - # Set show_function_calls if it is not set on the llm - if self.llm.show_function_calls is None: - self.llm.show_function_calls = self.show_function_calls - - def add_agents_to_llm(self) -> None: + def add_tools_to_llm(self) -> None: if self.llm is None: return - if self.agents is not None and len(self.agents) > 0: - for agent in self.agents: - self.llm.add_agent(agent) + if self.function_calls and self.default_functions: + default_func_list: List[Callable] = [ + self.get_last_n_chats, + self.search_knowledge_base, + ] + for func in default_func_list: + self.llm.add_tool(func) - # Set function call to auto if it is not set - if self.llm.function_call is None: - self.llm.function_call = "auto" + # Set show_function_calls if it is not set on the llm + if self.llm.show_function_calls is None and self.show_function_calls is not None: + self.llm.show_function_calls = self.show_function_calls - # Set show_function_calls if it is not set on the llm - if self.llm.show_function_calls is None: - self.llm.show_function_calls = self.show_function_calls + # Set tool_choice to auto if it is not set on the llm + if self.llm.tool_choice is None and self.tool_choice is not None: + self.llm.tool_choice = self.tool_choice def get_system_prompt(self) -> Optional[str]: """Return the system prompt for the task""" @@ -287,8 +264,7 @@ def _run(self, message: Optional[Union[List[Dict], str]] = None, stream: bool = # -*- Prepare the task self.set_task_id() - self.add_functions_to_llm() - self.add_agents_to_llm() + self.add_tools_to_llm() # -*- Build the system prompt system_prompt = self.get_system_prompt() diff --git a/phi/tool/__init__.py b/phi/tool/__init__.py new file mode 100644 index 000000000..044f3f5b1 --- /dev/null +++ b/phi/tool/__init__.py @@ -0,0 +1 @@ +from phi.tool.tool import Tool diff --git a/phi/assistant/tool/arxiv.py b/phi/tool/arxiv.py similarity index 96% rename from phi/assistant/tool/arxiv.py rename to phi/tool/arxiv.py index 289b35fc8..508aadc0e 100644 --- a/phi/assistant/tool/arxiv.py +++ b/phi/tool/arxiv.py @@ -3,11 +3,11 @@ from phi.document import Document from phi.knowledge.arxiv import ArxivKnowledgeBase -from phi.assistant.tool.registry import ToolRegistry +from phi.tool.registry import ToolRegistry from phi.utils.log import logger -class ArxivTools(ToolRegistry): +class ArxivTool(ToolRegistry): def __init__(self, knowledge_base: Optional[ArxivKnowledgeBase] = None): super().__init__(name="arxiv_tools") self.knowledge_base: Optional[ArxivKnowledgeBase] = knowledge_base diff --git a/phi/assistant/tool/duckdb.py b/phi/tool/duckdb.py similarity index 99% rename from phi/assistant/tool/duckdb.py rename to phi/tool/duckdb.py index 51eb0ee29..ac374e7dd 100644 --- a/phi/assistant/tool/duckdb.py +++ b/phi/tool/duckdb.py @@ -1,6 +1,6 @@ from typing import Optional, Tuple -from phi.assistant.tool.registry import ToolRegistry +from phi.tool.registry import ToolRegistry from phi.utils.log import logger try: @@ -9,7 +9,7 @@ raise ImportError("`duckdb` not installed. Please install it using `pip install duckdb`.") -class DuckDbTools(ToolRegistry): +class DuckDbTool(ToolRegistry): def __init__( self, db_path: str = ":memory:", diff --git a/phi/assistant/tool/email.py b/phi/tool/email.py similarity index 96% rename from phi/assistant/tool/email.py rename to phi/tool/email.py index 284a8e220..9313df0bf 100644 --- a/phi/assistant/tool/email.py +++ b/phi/tool/email.py @@ -1,10 +1,10 @@ from typing import Optional -from phi.assistant.tool.registry import ToolRegistry +from phi.tool.registry import ToolRegistry from phi.utils.log import logger -class EmailTools(ToolRegistry): +class EmailTool(ToolRegistry): def __init__( self, receiver_email: Optional[str] = None, diff --git a/phi/assistant/function.py b/phi/tool/function.py similarity index 90% rename from phi/assistant/function.py rename to phi/tool/function.py index ffb90a4e8..dcb0fcbbd 100644 --- a/phi/assistant/function.py +++ b/phi/tool/function.py @@ -4,19 +4,8 @@ from phi.utils.log import logger -class Tool(BaseModel): - """Model for Assistant Tools""" - - # The type of tool - type: str - function: Optional[Dict[str, Any]] = None - - def to_dict(self) -> Dict[str, Any]: - return self.model_dump(exclude_none=True) - - class Function(BaseModel): - """Model for Assistant functions""" + """Model for Functions""" # The name of the function to be called. # Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. @@ -53,7 +42,7 @@ def from_callable(cls, c: Callable) -> "Function": class FunctionCall(BaseModel): - """Model for Assistant function calls""" + """Model for Function Calls""" # The function to be called. function: Function diff --git a/phi/assistant/tool/phi.py b/phi/tool/phi.py similarity index 98% rename from phi/assistant/tool/phi.py rename to phi/tool/phi.py index c691ffe7a..b826390b8 100644 --- a/phi/assistant/tool/phi.py +++ b/phi/tool/phi.py @@ -1,11 +1,11 @@ import uuid from typing import Optional -from phi.assistant.tool.registry import ToolRegistry +from phi.tool.registry import ToolRegistry from phi.utils.log import logger -class PhiTools(ToolRegistry): +class PhiTool(ToolRegistry): def __init__(self): super().__init__(name="phi_tools") self.register(self.create_new_app) diff --git a/phi/assistant/tool/registry.py b/phi/tool/registry.py similarity index 89% rename from phi/assistant/tool/registry.py rename to phi/tool/registry.py index 9c562fc01..162dc9514 100644 --- a/phi/assistant/tool/registry.py +++ b/phi/tool/registry.py @@ -1,12 +1,12 @@ from collections import OrderedDict from typing import Callable, Dict -from phi.assistant.function import Function +from phi.tool.function import Function from phi.utils.log import logger class ToolRegistry: - def __init__(self, name: str = "default_registry"): + def __init__(self, name: str = "default_tools"): self.name: str = name self.functions: Dict[str, Function] = OrderedDict() diff --git a/phi/assistant/tool/shell.py b/phi/tool/shell.py similarity index 92% rename from phi/assistant/tool/shell.py rename to phi/tool/shell.py index 8f2047319..bccf6868f 100644 --- a/phi/assistant/tool/shell.py +++ b/phi/tool/shell.py @@ -1,10 +1,10 @@ from typing import List -from phi.assistant.tool.registry import ToolRegistry +from phi.tool.registry import ToolRegistry from phi.utils.log import logger -class ShellTools(ToolRegistry): +class ShellTool(ToolRegistry): def __init__(self): super().__init__(name="shell_tools") self.register(self.run_shell_command) diff --git a/phi/assistant/tool/tool.py b/phi/tool/tool.py similarity index 77% rename from phi/assistant/tool/tool.py rename to phi/tool/tool.py index 7c353ad76..48f8db79c 100644 --- a/phi/assistant/tool/tool.py +++ b/phi/tool/tool.py @@ -3,10 +3,11 @@ class Tool(BaseModel): - """Model for Assistant Tools""" + """Model for Tools""" # The type of tool type: str + # The function to be called if type = "function" function: Optional[Dict[str, Any]] = None def to_dict(self) -> Dict[str, Any]: diff --git a/phi/assistant/tool/website.py b/phi/tool/website.py similarity index 95% rename from phi/assistant/tool/website.py rename to phi/tool/website.py index cbea35a5d..b9d6a79de 100644 --- a/phi/assistant/tool/website.py +++ b/phi/tool/website.py @@ -3,11 +3,11 @@ from phi.document import Document from phi.knowledge.website import WebsiteKnowledgeBase -from phi.assistant.tool.registry import ToolRegistry +from phi.tool.registry import ToolRegistry from phi.utils.log import logger -class WebsiteTools(ToolRegistry): +class WebsiteTool(ToolRegistry): def __init__(self, knowledge_base: Optional[WebsiteKnowledgeBase] = None): super().__init__(name="website_tools") self.knowledge_base: Optional[WebsiteKnowledgeBase] = knowledge_base diff --git a/phi/assistant/tool/wikipedia.py b/phi/tool/wikipedia.py similarity index 96% rename from phi/assistant/tool/wikipedia.py rename to phi/tool/wikipedia.py index cedd12d30..715dbef6d 100644 --- a/phi/assistant/tool/wikipedia.py +++ b/phi/tool/wikipedia.py @@ -3,11 +3,11 @@ from phi.document import Document from phi.knowledge.wikipedia import WikipediaKnowledgeBase -from phi.assistant.tool.registry import ToolRegistry +from phi.tool.registry import ToolRegistry from phi.utils.log import logger -class WikipediaAgent(ToolRegistry): +class WikipediaTool(ToolRegistry): def __init__(self, knowledge_base: Optional[WikipediaKnowledgeBase] = None): super().__init__(name="wikipedia_tools") self.knowledge_base: Optional[WikipediaKnowledgeBase] = knowledge_base diff --git a/phi/utils/functions.py b/phi/utils/functions.py index 0b783c217..5be259f27 100644 --- a/phi/utils/functions.py +++ b/phi/utils/functions.py @@ -1,7 +1,7 @@ import json from typing import Optional, Dict, Any -from phi.llm.schemas import Function, FunctionCall +from phi.tool.function import Function, FunctionCall from phi.utils.log import logger From cd5a3448b58cb1202b752d2a6b59bc7117864da1 Mon Sep 17 00:00:00 2001 From: Ashpreet Bedi Date: Fri, 10 Nov 2023 13:47:33 +0000 Subject: [PATCH 2/5] v2.0.36 --- phi/llm/tool/__init__.py | 0 phi/llm/tool/base.py | 12 ------------ 2 files changed, 12 deletions(-) delete mode 100644 phi/llm/tool/__init__.py delete mode 100644 phi/llm/tool/base.py diff --git a/phi/llm/tool/__init__.py b/phi/llm/tool/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/phi/llm/tool/base.py b/phi/llm/tool/base.py deleted file mode 100644 index c32e4f7de..000000000 --- a/phi/llm/tool/base.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Any, Dict -from pydantic import BaseModel - - -class BaseTool(BaseModel): - """Model for LLM Tools""" - - # The type of tool - type: str - - def to_dict(self) -> Dict[str, Any]: - return self.model_dump(exclude_none=True) From b01a803bfe52991d4ef4c09be91816ce8eb75664 Mon Sep 17 00:00:00 2001 From: Ashpreet Bedi Date: Fri, 10 Nov 2023 14:27:59 +0000 Subject: [PATCH 3/5] v2.0.36 --- phi/agent/__init__.py | 1 + phi/agent/agent.py | 27 ++ phi/{tool => agent}/arxiv.py | 6 +- phi/{tool => agent}/duckdb.py | 12 +- phi/{tool => agent}/email.py | 6 +- phi/agent/google.py | 18 ++ phi/{tool => agent}/phi.py | 6 +- phi/agent/pubmed.py | 19 ++ phi/{tool => agent}/shell.py | 6 +- phi/{tool => agent}/website.py | 6 +- phi/{tool => agent}/wikipedia.py | 6 +- phi/ai/phi_ai.py | 8 +- phi/api/ai.py | 2 +- phi/assistant/assistant.py | 8 +- phi/assistant/run.py | 69 ++-- phi/conversation/conversation.py | 16 +- phi/conversation/memory/base.py | 3 +- phi/llm/agent/__init__.py | 0 phi/llm/agent/arxiv.py | 53 +++ phi/{tool/registry.py => llm/agent/base.py} | 4 +- phi/llm/agent/duckdb.py | 339 ++++++++++++++++++++ phi/llm/agent/email.py | 59 ++++ phi/llm/agent/google.py | 18 ++ phi/llm/agent/phi.py | 116 +++++++ phi/llm/agent/pubmed.py | 19 ++ phi/llm/agent/shell.py | 34 ++ phi/llm/agent/website.py | 50 +++ phi/llm/agent/wikipedia.py | 54 ++++ phi/llm/aws/bedrock.py | 2 +- phi/llm/base.py | 10 +- phi/llm/{schemas.py => message.py} | 16 +- phi/llm/openai/chat.py | 2 +- phi/llm/references.py | 13 + phi/llm/task/llm_task.py | 22 +- phi/llm/task/memory/base.py | 3 +- 35 files changed, 938 insertions(+), 95 deletions(-) create mode 100644 phi/agent/__init__.py create mode 100644 phi/agent/agent.py rename phi/{tool => agent}/arxiv.py (94%) rename phi/{tool => agent}/duckdb.py (97%) rename phi/{tool => agent}/email.py (94%) create mode 100644 phi/agent/google.py rename phi/{tool => agent}/phi.py (97%) create mode 100644 phi/agent/pubmed.py rename phi/{tool => agent}/shell.py (90%) rename phi/{tool => agent}/website.py (93%) rename phi/{tool => agent}/wikipedia.py (94%) create mode 100644 phi/llm/agent/__init__.py create mode 100644 phi/llm/agent/arxiv.py rename phi/{tool/registry.py => llm/agent/base.py} (91%) create mode 100644 phi/llm/agent/duckdb.py create mode 100644 phi/llm/agent/email.py create mode 100644 phi/llm/agent/google.py create mode 100644 phi/llm/agent/phi.py create mode 100644 phi/llm/agent/pubmed.py create mode 100644 phi/llm/agent/shell.py create mode 100644 phi/llm/agent/website.py create mode 100644 phi/llm/agent/wikipedia.py rename phi/llm/{schemas.py => message.py} (90%) create mode 100644 phi/llm/references.py diff --git a/phi/agent/__init__.py b/phi/agent/__init__.py new file mode 100644 index 000000000..f312a88d6 --- /dev/null +++ b/phi/agent/__init__.py @@ -0,0 +1 @@ +from phi.agent.agent import Agent diff --git a/phi/agent/agent.py b/phi/agent/agent.py new file mode 100644 index 000000000..09f37fc83 --- /dev/null +++ b/phi/agent/agent.py @@ -0,0 +1,27 @@ +from collections import OrderedDict +from typing import Callable, Dict + +from phi.tool.function import Function +from phi.utils.log import logger + + +class Agent: + def __init__(self, name: str = "base_agent"): + self.name: str = name + self.functions: Dict[str, Function] = OrderedDict() + + def register(self, function: Callable): + try: + f = Function.from_callable(function) + self.functions[f.name] = f + logger.debug(f"Function: {f.name} registered with {self.name}") + # logger.debug(f"Json Schema: {f.to_dict()}") + except Exception as e: + logger.warning(f"Failed to create Function for: {function.__name__}") + raise e + + def __repr__(self): + return f"<{self.__class__.__name__} name={self.name} functions={list(self.functions.keys())}>" + + def __str__(self): + return self.__repr__() diff --git a/phi/tool/arxiv.py b/phi/agent/arxiv.py similarity index 94% rename from phi/tool/arxiv.py rename to phi/agent/arxiv.py index 508aadc0e..13a0117cb 100644 --- a/phi/tool/arxiv.py +++ b/phi/agent/arxiv.py @@ -3,13 +3,13 @@ from phi.document import Document from phi.knowledge.arxiv import ArxivKnowledgeBase -from phi.tool.registry import ToolRegistry +from phi.agent import Agent from phi.utils.log import logger -class ArxivTool(ToolRegistry): +class ArxivAgent(Agent): def __init__(self, knowledge_base: Optional[ArxivKnowledgeBase] = None): - super().__init__(name="arxiv_tools") + super().__init__(name="arxiv_agent") self.knowledge_base: Optional[ArxivKnowledgeBase] = knowledge_base if self.knowledge_base is not None and isinstance(self.knowledge_base, ArxivKnowledgeBase): diff --git a/phi/tool/duckdb.py b/phi/agent/duckdb.py similarity index 97% rename from phi/tool/duckdb.py rename to phi/agent/duckdb.py index ac374e7dd..3da8b7c3a 100644 --- a/phi/tool/duckdb.py +++ b/phi/agent/duckdb.py @@ -1,6 +1,6 @@ from typing import Optional, Tuple -from phi.tool.registry import ToolRegistry +from phi.agent import Agent from phi.utils.log import logger try: @@ -9,14 +9,14 @@ raise ImportError("`duckdb` not installed. Please install it using `pip install duckdb`.") -class DuckDbTool(ToolRegistry): +class DuckDbAgent(Agent): def __init__( self, db_path: str = ":memory:", s3_region: str = "us-east-1", duckdb_connection: Optional[duckdb.DuckDBPyConnection] = None, ): - super().__init__(name="duckdb_tools") + super().__init__(name="duckdb_agent") self.db_path: str = db_path self.s3_region: str = s3_region @@ -329,9 +329,9 @@ def full_text_search(self, table_name: str, unique_key: str, search_text: str) - """ logger.debug(f"Running full_text_search for {search_text} in {table_name}") search_text_statement = f"""SELECT fts_main_corpus.match_bm25({unique_key}, '{search_text}') AS score,* - FROM {table_name} - WHERE score IS NOT NULL - ORDER BY score;""" + FROM {table_name} + WHERE score IS NOT NULL + ORDER BY score;""" logger.debug(f"Running {search_text_statement}") result = self.run_query(search_text_statement) diff --git a/phi/tool/email.py b/phi/agent/email.py similarity index 94% rename from phi/tool/email.py rename to phi/agent/email.py index 9313df0bf..4b49bb939 100644 --- a/phi/tool/email.py +++ b/phi/agent/email.py @@ -1,10 +1,10 @@ from typing import Optional -from phi.tool.registry import ToolRegistry +from phi.agent import Agent from phi.utils.log import logger -class EmailTool(ToolRegistry): +class EmailAgent(Agent): def __init__( self, receiver_email: Optional[str] = None, @@ -12,7 +12,7 @@ def __init__( sender_email: Optional[str] = None, sender_passkey: Optional[str] = None, ): - super().__init__(name="email_tools") + super().__init__(name="email_agent") self.receiver_email: Optional[str] = receiver_email self.sender_name: Optional[str] = sender_name self.sender_email: Optional[str] = sender_email diff --git a/phi/agent/google.py b/phi/agent/google.py new file mode 100644 index 000000000..0fe66de98 --- /dev/null +++ b/phi/agent/google.py @@ -0,0 +1,18 @@ +from phi.agent import Agent +from phi.utils.log import logger + + +class GoogleAgent(Agent): + def __init__(self): + super().__init__(name="google_agent") + self.register(self.get_result_from_google) + + def get_result_from_google(self, query: str) -> str: + """Gets the result for a query from Google. + Use this function to find an answer when not available in the knowledge base. + + :param query: The query to search for. + :return: The result from Google. + """ + logger.info(f"Searching google for: {query}") + return "Sorry, this capability is not available yet." diff --git a/phi/tool/phi.py b/phi/agent/phi.py similarity index 97% rename from phi/tool/phi.py rename to phi/agent/phi.py index b826390b8..9636a419e 100644 --- a/phi/tool/phi.py +++ b/phi/agent/phi.py @@ -1,13 +1,13 @@ import uuid from typing import Optional -from phi.tool.registry import ToolRegistry +from phi.agent import Agent from phi.utils.log import logger -class PhiTool(ToolRegistry): +class PhiAgent(Agent): def __init__(self): - super().__init__(name="phi_tools") + super().__init__(name="phi_agent") self.register(self.create_new_app) self.register(self.start_user_workspace) self.register(self.validate_phi_is_ready) diff --git a/phi/agent/pubmed.py b/phi/agent/pubmed.py new file mode 100644 index 000000000..ac583fe8a --- /dev/null +++ b/phi/agent/pubmed.py @@ -0,0 +1,19 @@ +from phi.agent import Agent +from phi.utils.log import logger + + +class PubMedAgent(Agent): + def __init__(self): + super().__init__(name="pubmed_agent") + self.register(self.get_articles_from_pubmed) + + def get_articles_from_pubmed(self, query: str, num_articles: int = 2) -> str: + """Gets the abstract for articles related to a query from PubMed: a database of biomedical literature + Use this function to find information about a medical concept when not available in the knowledge base or Google + + :param query: The query to get related articles for. + :param num_articles: The number of articles to return. + :return: JSON string containing the articles + """ + logger.info(f"Searching Pubmed for: {query}") + return "Sorry, this capability is not available yet." diff --git a/phi/tool/shell.py b/phi/agent/shell.py similarity index 90% rename from phi/tool/shell.py rename to phi/agent/shell.py index bccf6868f..31c3081eb 100644 --- a/phi/tool/shell.py +++ b/phi/agent/shell.py @@ -1,12 +1,12 @@ from typing import List -from phi.tool.registry import ToolRegistry +from phi.agent import Agent from phi.utils.log import logger -class ShellTool(ToolRegistry): +class ShellAgent(Agent): def __init__(self): - super().__init__(name="shell_tools") + super().__init__(name="shell_agent") self.register(self.run_shell_command) def run_shell_command(self, args: List[str], tail: int = 100) -> str: diff --git a/phi/tool/website.py b/phi/agent/website.py similarity index 93% rename from phi/tool/website.py rename to phi/agent/website.py index b9d6a79de..76b6e7d80 100644 --- a/phi/tool/website.py +++ b/phi/agent/website.py @@ -3,13 +3,13 @@ from phi.document import Document from phi.knowledge.website import WebsiteKnowledgeBase -from phi.tool.registry import ToolRegistry +from phi.agent import Agent from phi.utils.log import logger -class WebsiteTool(ToolRegistry): +class WebsiteAgent(Agent): def __init__(self, knowledge_base: Optional[WebsiteKnowledgeBase] = None): - super().__init__(name="website_tools") + super().__init__(name="website_agent") self.knowledge_base: Optional[WebsiteKnowledgeBase] = knowledge_base if self.knowledge_base is not None and isinstance(self.knowledge_base, WebsiteKnowledgeBase): diff --git a/phi/tool/wikipedia.py b/phi/agent/wikipedia.py similarity index 94% rename from phi/tool/wikipedia.py rename to phi/agent/wikipedia.py index 715dbef6d..c773f7cd4 100644 --- a/phi/tool/wikipedia.py +++ b/phi/agent/wikipedia.py @@ -3,13 +3,13 @@ from phi.document import Document from phi.knowledge.wikipedia import WikipediaKnowledgeBase -from phi.tool.registry import ToolRegistry +from phi.agent import Agent from phi.utils.log import logger -class WikipediaTool(ToolRegistry): +class WikipediaAgent(Agent): def __init__(self, knowledge_base: Optional[WikipediaKnowledgeBase] = None): - super().__init__(name="wikipedia_tools") + super().__init__(name="wikipedia_agent") self.knowledge_base: Optional[WikipediaKnowledgeBase] = knowledge_base if self.knowledge_base is not None and isinstance(self.knowledge_base, WikipediaKnowledgeBase): diff --git a/phi/ai/phi_ai.py b/phi/ai/phi_ai.py index a86c605d8..fcfc497de 100644 --- a/phi/ai/phi_ai.py +++ b/phi/ai/phi_ai.py @@ -13,10 +13,10 @@ from phi.cli.config import PhiCliConfig from phi.cli.console import console from phi.cli.settings import phi_cli_settings -from phi.llm.schemas import Message +from phi.llm.message import Message from phi.tool.function import Function, FunctionCall -from phi.tool.shell import ShellTool -from phi.tool.phi import PhiTool +from phi.agent.shell import ShellAgent +from phi.agent.phi import PhiAgent from phi.workspace.config import WorkspaceConfig from phi.utils.log import logger from phi.utils.functions import get_function_call @@ -42,7 +42,7 @@ def __init__( _active_workspace = _phi_config.get_active_ws_config() self.conversation_db: Optional[List[Dict[str, Any]]] = None - self.functions: Dict[str, Function] = {**ShellTool().functions, **PhiTool().functions} + self.functions: Dict[str, Function] = {**ShellAgent().functions, **PhiAgent().functions} _conversation_id = None _conversation_history = None diff --git a/phi/api/ai.py b/phi/api/ai.py index 99b950e2f..a041cfdf9 100644 --- a/phi/api/ai.py +++ b/phi/api/ai.py @@ -10,7 +10,7 @@ ConversationCreateResponse, ) from phi.api.schemas.user import UserSchema -from phi.llm.schemas import Message +from phi.llm.message import Message from phi.tool.function import Function from phi.utils.log import logger diff --git a/phi/assistant/assistant.py b/phi/assistant/assistant.py index 8f550feeb..697bd8e4c 100644 --- a/phi/assistant/assistant.py +++ b/phi/assistant/assistant.py @@ -3,12 +3,12 @@ from pydantic import BaseModel, ConfigDict, field_validator, model_validator +from phi.agent import Agent from phi.assistant.file import File from phi.assistant.row import AssistantRow from phi.assistant.storage import AssistantStorage from phi.assistant.exceptions import AssistantIdNotSet from phi.tool import Tool -from phi.tool.registry import ToolRegistry from phi.tool.function import Function from phi.knowledge.base import KnowledgeBase from phi.utils.log import logger, set_log_level_to_debug @@ -42,7 +42,7 @@ class Assistant(BaseModel): # -*- Assistant Tools # A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant. # Tools can be of types code_interpreter, retrieval, or function. - tools: Optional[List[Union[Tool, Dict, Callable, ToolRegistry]]] = None + tools: Optional[List[Union[Tool, Dict, Callable, Agent]]] = None # -*- Functions available to the Assistant to call # Functions provided from the tools. Note: These are not sent to the LLM API. functions: Optional[Dict[str, Function]] = None @@ -101,7 +101,7 @@ def extract_functions_from_tools(self) -> "Assistant": for tool in self.tools: if self.functions is None: self.functions = {} - if isinstance(tool, ToolRegistry): + if isinstance(tool, Agent): self.functions.update(tool.functions) logger.debug(f"Tools from {tool.name} added to Assistant.") elif callable(tool): @@ -133,7 +133,7 @@ def get_tools_for_api(self) -> Optional[List[Dict[str, Any]]]: elif callable(tool): func = Function.from_callable(tool) tools_for_api.append({"type": "function", "function": func.to_dict()}) - elif isinstance(tool, ToolRegistry): + elif isinstance(tool, Agent): for _f in tool.functions.values(): tools_for_api.append({"type": "function", "function": _f.to_dict()}) return tools_for_api diff --git a/phi/assistant/run.py b/phi/assistant/run.py index c0f91675a..cd4417a25 100644 --- a/phi/assistant/run.py +++ b/phi/assistant/run.py @@ -3,10 +3,10 @@ from pydantic import BaseModel, ConfigDict, model_validator +from phi.agent import Agent from phi.assistant.assistant import Assistant from phi.assistant.exceptions import ThreadIdNotSet, AssistantIdNotSet, RunIdNotSet from phi.tool import Tool -from phi.tool.registry import ToolRegistry from phi.tool.function import Function from phi.utils.functions import get_function_call from phi.utils.log import logger @@ -73,9 +73,9 @@ class Run(BaseModel): instructions: Optional[str] = None # Override the tools the assistant can use for this run. # This is useful for modifying the behavior on a per-run basis. - tools: Optional[List[Union[Tool, Dict, Callable, ToolRegistry]]] = None + tools: Optional[List[Union[Tool, Dict, Callable, Agent]]] = None # Functions the Run may call. - _function_map: Optional[Dict[str, Function]] = None + functions: Optional[Dict[str, Function]] = None # The last error associated with this run. Will be null if there are no errors. last_error: Optional[LastError] = None @@ -99,24 +99,19 @@ class Run(BaseModel): def client(self) -> OpenAI: return self.openai or OpenAI() - def add_function(self, f: Function) -> None: - if self._function_map is None: - self._function_map = {} - self._function_map[f.name] = f - logger.debug(f"Added function {f.name} to Run") - @model_validator(mode="after") - def add_functions_to_assistant(self) -> "Run": + def extract_functions_from_tools(self) -> "Run": if self.tools is not None: for tool in self.tools: - if callable(tool): - f = Function.from_callable(tool) - self.add_function(f) - elif isinstance(tool, ToolRegistry): - if self._function_map is None: - self._function_map = {} - self._function_map.update(tool.functions) + if self.functions is None: + self.functions = {} + if isinstance(tool, Agent): + self.functions.update(tool.functions) logger.debug(f"Tools from {tool.name} added to Assistant.") + elif callable(tool): + f = Function.from_callable(tool) + self.functions[f.name] = f + logger.debug(f"Added function {f.name} to Assistant") return self def load_from_storage(self): @@ -137,6 +132,24 @@ def load_from_openai(self, openai_run: OpenAIRun): self.file_ids = openai_run.file_ids self.openai_run = openai_run + def get_tools_for_api(self) -> Optional[List[Dict[str, Any]]]: + if self.tools is None: + return None + + tools_for_api = [] + for tool in self.tools: + if isinstance(tool, Tool): + tools_for_api.append(tool.to_dict()) + elif isinstance(tool, dict): + tools_for_api.append(tool) + elif callable(tool): + func = Function.from_callable(tool) + tools_for_api.append({"type": "function", "function": func.to_dict()}) + elif isinstance(tool, Agent): + for _f in tool.functions.values(): + tools_for_api.append({"type": "function", "function": _f.to_dict()}) + return tools_for_api + def create( self, thread_id: Optional[str] = None, assistant: Optional[Assistant] = None, assistant_id: Optional[str] = None ) -> "Run": @@ -156,19 +169,7 @@ def create( if self.instructions is not None: request_body["instructions"] = self.instructions if self.tools is not None: - _tools = [] - for _tool in self.tools: - if isinstance(_tool, Tool): - _tools.append(_tool.to_dict()) - elif isinstance(_tool, dict): - _tools.append(_tool) - elif callable(_tool): - func = Function.from_callable(_tool) - _tools.append({"type": "function", "function": func.to_dict()}) - elif isinstance(_tool, ToolRegistry): - for _f in _tool.functions.values(): - _tools.append({"type": "function", "function": _f.to_dict()}) - request_body["tools"] = _tools + request_body["tools"] = self.get_tools_for_api() if self.metadata is not None: request_body["metadata"] = self.metadata @@ -301,10 +302,16 @@ def run( tool_outputs = [] for tool_call in tool_calls: if tool_call.type == "function": + run_functions = self.assistant.functions + if self.functions is not None: + if run_functions is not None: + run_functions.update(self.functions) + else: + run_functions = self.functions function_call = get_function_call( name=tool_call.function.name, arguments=tool_call.function.arguments, - functions=self.assistant.functions, + functions=run_functions, ) if function_call is None: logger.error(f"Function {tool_call.function.name} not found") diff --git a/phi/conversation/conversation.py b/phi/conversation/conversation.py index 30964f0af..4c8f9dcf0 100644 --- a/phi/conversation/conversation.py +++ b/phi/conversation/conversation.py @@ -4,6 +4,7 @@ from pydantic import BaseModel, ConfigDict, field_validator, model_validator, Field +from phi.agent import Agent from phi.conversation.memory import ConversationMemory from phi.conversation.schemas import ConversationRow from phi.conversation.storage import ConversationStorage @@ -11,10 +12,10 @@ from phi.knowledge.base import KnowledgeBase from phi.llm.base import LLM from phi.llm.openai import OpenAIChat -from phi.llm.schemas import Message, References +from phi.llm.message import Message +from phi.llm.references import References from phi.llm.task.llm_task import LLMTask from phi.tool.tool import Tool -from phi.tool.registry import ToolRegistry from phi.utils.format_str import remove_indent from phi.utils.log import logger, set_log_level_to_debug from phi.utils.timer import Timer @@ -82,7 +83,7 @@ class Conversation(BaseModel): # A list of tools provided to the LLM. # Currently, only functions are supported as a tool. # Use this to provide a list of functions the model may generate JSON inputs for. - tools: Optional[List[Union[Tool, Dict, Callable, ToolRegistry]]] = None + tools: Optional[List[Union[Tool, Dict, Callable, Agent]]] = None # Controls which (if any) function is called by the model. # "none" means the model will not call a function and instead generates a message. # "auto" means the model can pick between generating a message or calling a function. @@ -91,6 +92,11 @@ class Conversation(BaseModel): # "none" is the default when no functions are present. "auto" is the default if functions are present. tool_choice: Optional[Union[str, Dict[str, Any]]] = None + # -*- Agents + # A list of agents provided to the LLM + # These are added to the tools list + agents: Optional[List[Agent]] = None + # -*- Tasks # Generate a response using tasks instead of a prompt tasks: Optional[List[LLMTask]] = None @@ -170,6 +176,10 @@ def add_tools_to_llm(self) -> "Conversation": for tool in self.tools: self.llm.add_tool(tool) + if self.agents is not None: + for agent in self.agents: + self.llm.add_tool(agent) + if self.function_calls and self.default_functions: default_func_list: List[Callable] = [ self.get_last_n_chats, diff --git a/phi/conversation/memory/base.py b/phi/conversation/memory/base.py index 18335fe42..37a33cf44 100644 --- a/phi/conversation/memory/base.py +++ b/phi/conversation/memory/base.py @@ -2,7 +2,8 @@ from pydantic import BaseModel -from phi.llm.schemas import Message, References +from phi.llm.message import Message +from phi.llm.references import References class ConversationMemory(BaseModel): diff --git a/phi/llm/agent/__init__.py b/phi/llm/agent/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/phi/llm/agent/arxiv.py b/phi/llm/agent/arxiv.py new file mode 100644 index 000000000..e7b2660bb --- /dev/null +++ b/phi/llm/agent/arxiv.py @@ -0,0 +1,53 @@ +import json +from typing import List, Optional + +from phi.document import Document +from phi.knowledge.arxiv import ArxivKnowledgeBase +from phi.llm.agent.base import BaseAgent +from phi.utils.log import logger + + +class ArxivAgent(BaseAgent): + def __init__(self, knowledge_base: Optional[ArxivKnowledgeBase] = None): + super().__init__(name="arxiv_agent") + self.knowledge_base: Optional[ArxivKnowledgeBase] = knowledge_base + + if self.knowledge_base is not None and isinstance(self.knowledge_base, ArxivKnowledgeBase): + self.register(self.search_arxiv_and_update_knowledge_base) + else: + self.register(self.search_arxiv) + + def search_arxiv_and_update_knowledge_base(self, topic: str) -> str: + """This function searches arXiv for a topic, adds the results to the knowledge base and returns them. + + USE THIS FUNCTION TO GET INFORMATION WHICH DOES NOT EXIST. + + :param topic: The topic to search arXiv and add to knowledge base. + :return: Relevant documents from arXiv knowledge base. + """ + if self.knowledge_base is None: + return "Knowledge base not provided" + + logger.debug(f"Adding to knowledge base: {topic}") + self.knowledge_base.queries.append(topic) + logger.debug("Loading knowledge base.") + self.knowledge_base.load(recreate=False) + logger.debug(f"Searching knowledge base: {topic}") + relevant_docs: List[Document] = self.knowledge_base.search(query=topic) + return json.dumps([doc.to_dict() for doc in relevant_docs]) + + def search_arxiv(self, query: str, max_results: int = 5) -> str: + """ + Searches arXiv for a query. + + :param query: The query to search for. + :param max_results: The maximum number of results to return. + :return: Relevant documents from arXiv. + """ + from phi.document.reader.arxiv import ArxivReader + + arxiv = ArxivReader(max_results=max_results) + + logger.debug(f"Searching arxiv for: {query}") + relevant_docs: List[Document] = arxiv.read(query=query) + return json.dumps([doc.to_dict() for doc in relevant_docs]) diff --git a/phi/tool/registry.py b/phi/llm/agent/base.py similarity index 91% rename from phi/tool/registry.py rename to phi/llm/agent/base.py index 162dc9514..427ae4bb7 100644 --- a/phi/tool/registry.py +++ b/phi/llm/agent/base.py @@ -5,8 +5,8 @@ from phi.utils.log import logger -class ToolRegistry: - def __init__(self, name: str = "default_tools"): +class BaseAgent: + def __init__(self, name: str = "base_agent"): self.name: str = name self.functions: Dict[str, Function] = OrderedDict() diff --git a/phi/llm/agent/duckdb.py b/phi/llm/agent/duckdb.py new file mode 100644 index 000000000..9f0abe157 --- /dev/null +++ b/phi/llm/agent/duckdb.py @@ -0,0 +1,339 @@ +from typing import Optional, Tuple + +from phi.llm.agent.base import BaseAgent +from phi.utils.log import logger + +try: + import duckdb +except ImportError: + raise ImportError("`duckdb` not installed. Please install it using `pip install duckdb`.") + + +class DuckDbAgent(BaseAgent): + def __init__( + self, + db_path: str = ":memory:", + s3_region: str = "us-east-1", + duckdb_connection: Optional[duckdb.DuckDBPyConnection] = None, + ): + super().__init__(name="duckdb_registry") + + self.db_path: str = db_path + self.s3_region: str = s3_region + self._duckdb_connection: Optional[duckdb.DuckDBPyConnection] = duckdb_connection + + self.register(self.run_duckdb_query) + self.register(self.show_tables) + self.register(self.describe_table) + self.register(self.inspect_query) + self.register(self.describe_table_or_view) + self.register(self.export_table_as) + self.register(self.summarize_table) + self.register(self.create_fts_index) + self.register(self.full_text_search) + + @property + def duckdb_connection(self) -> duckdb.DuckDBPyConnection: + """ + Returns the duckdb connection + + :return duckdb.DuckDBPyConnection: duckdb connection + """ + if self._duckdb_connection is None: + self._duckdb_connection = duckdb.connect(self.db_path) + try: + self._duckdb_connection.sql("INSTALL httpfs;") + self._duckdb_connection.sql("LOAD httpfs;") + self._duckdb_connection.sql(f"SET s3_region='{self.s3_region}';") + except Exception as e: + logger.exception(e) + logger.warning("Failed to install httpfs extension. Only local files will be supported") + + return self._duckdb_connection + + def run_duckdb_query(self, query: str) -> str: + """Function to run SQL queries against a duckdb database + + :param query: SQL query to run + :return: Result of the query + """ + + # -*- Format the SQL Query + # Remove backticks + formatted_sql = query.replace("`", "") + # If there are multiple statements, only run the first one + formatted_sql = formatted_sql.split(";")[0] + + try: + logger.debug(f"Running query: {formatted_sql}") + + query_result = self.duckdb_connection.sql(formatted_sql) + result_output = "No output" + if query_result is not None: + try: + results_as_python_objects = query_result.fetchall() + result_rows = [] + for row in results_as_python_objects: + if len(row) == 1: + result_rows.append(str(row[0])) + else: + result_rows.append(",".join(str(x) for x in row)) + + result_data = "\n".join(result_rows) + result_output = ",".join(query_result.columns) + "\n" + result_data + except AttributeError: + result_output = str(query_result) + + logger.debug(f"Query result: {result_output}") + return result_output + except duckdb.ProgrammingError as e: + return str(e) + except duckdb.Error as e: + return str(e) + except Exception as e: + return str(e) + + def show_tables(self) -> str: + """Function to show tables in the database + + :return: List of tables in the database + """ + stmt = "SHOW TABLES;" + tables = self.run_duckdb_query(stmt) + logger.debug(f"Tables: {tables}") + return tables + + def describe_table(self, table: str) -> str: + """Function to describe a table + + :param table: Table to describe + :return: Description of the table + """ + stmt = f"DESCRIBE {table};" + table_description = self.run_duckdb_query(stmt) + + logger.debug(f"Table description: {table_description}") + return f"{table}\n{table_description}" + + def summarize_table(self, table: str) -> str: + """Function to summarize the contents of a table + + :param table: Table to describe + :return: Description of the table + """ + stmt = f"SUMMARIZE SELECT * FROM {table};" + table_description = self.run_duckdb_query(stmt) + + logger.debug(f"Table description: {table_description}") + return f"{table}\n{table_description}" + + def inspect_query(self, query: str) -> str: + """Function to inspect a query and return the query plan. Always inspect your query before running them. + + :param query: Query to inspect + :return: Qeury plan + """ + stmt = f"explain {query};" + explain_plan = self.run_duckdb_query(stmt) + + logger.debug(f"Explain plan: {explain_plan}") + return explain_plan + + def describe_table_or_view(self, table: str): + """Function to describe a table or view + + :param table: Table or view to describe + :return: Description of the table or view + """ + stmt = f"select column_name, data_type from information_schema.columns where table_name='{table}';" + table_description = self.run_duckdb_query(stmt) + + logger.debug(f"Table description: {table_description}") + return f"{table}\n{table_description}" + + def load_local_path_to_table(self, path: str, table_name: Optional[str] = None) -> Tuple[str, str]: + """Load a local file into duckdb + + :param path: Path to load + :param table_name: Optional table name to use + :return: Table name, SQL statement used to load the file + """ + import os + + logger.debug(f"Loading {path} into duckdb") + + if table_name is None: + # Get the file name from the s3 path + file_name = path.split("/")[-1] + # Get the file name without extension from the s3 path + table_name, extension = os.path.splitext(file_name) + # If the table_name isn't a valid SQL identifier, we'll need to use something else + table_name = table_name.replace("-", "_").replace(".", "_").replace(" ", "_").replace("/", "_") + + create_statement = f"CREATE OR REPLACE TABLE '{table_name}' AS SELECT * FROM '{path}';" + self.run_duckdb_query(create_statement) + + logger.debug(f"Loaded {path} into duckdb as {table_name}") + # self.run_duckdb_query(f"SELECT * from {table_name};") + return table_name, create_statement + + def load_local_csv_to_table( + self, path: str, table_name: Optional[str] = None, delimiter: Optional[str] = None + ) -> Tuple[str, str]: + """Load a local CSV file into duckdb + + :param path: Path to load + :param table_name: Optional table name to use + :param delimiter: Optional delimiter to use + :return: Table name, SQL statement used to load the file + """ + import os + + logger.debug(f"Loading {path} into duckdb") + + if table_name is None: + # Get the file name from the s3 path + file_name = path.split("/")[-1] + # Get the file name without extension from the s3 path + table_name, extension = os.path.splitext(file_name) + # If the table_name isn't a valid SQL identifier, we'll need to use something else + table_name = table_name.replace("-", "_").replace(".", "_").replace(" ", "_").replace("/", "_") + + select_statement = f"SELECT * FROM read_csv('{path}'" + if delimiter is not None: + select_statement += f", delim='{delimiter}')" + else: + select_statement += ")" + + create_statement = f"CREATE OR REPLACE TABLE '{table_name}' AS {select_statement};" + self.run_duckdb_query(create_statement) + + logger.debug(f"Loaded CSV {path} into duckdb as {table_name}") + # self.run_duckdb_query(f"SELECT * from {table_name};") + return table_name, create_statement + + def load_s3_path_to_table(self, s3_path: str, table_name: Optional[str] = None) -> Tuple[str, str]: + """Load a file from S3 into duckdb + + :param s3_path: S3 path to load + :param table_name: Optional table name to use + :return: Table name, SQL statement used to load the file + """ + import os + + logger.debug(f"Loading {s3_path} into duckdb") + + if table_name is None: + # Get the file name from the s3 path + file_name = s3_path.split("/")[-1] + # Get the file name without extension from the s3 path + table_name, extension = os.path.splitext(file_name) + # If the table_name isn't a valid SQL identifier, we'll need to use something else + table_name = table_name.replace("-", "_").replace(".", "_").replace(" ", "_").replace("/", "_") + + create_statement = f"CREATE OR REPLACE TABLE '{table_name}' AS SELECT * FROM '{s3_path}';" + self.run_duckdb_query(create_statement) + + logger.debug(f"Loaded {s3_path} into duckdb as {table_name}") + # self.run_duckdb_query(f"SELECT * from {table_name};") + return table_name, create_statement + + def load_s3_csv_to_table( + self, s3_path: str, table_name: Optional[str] = None, delimiter: Optional[str] = None + ) -> Tuple[str, str]: + """Load a CSV file from S3 into duckdb + + :param s3_path: S3 path to load + :param table_name: Optional table name to use + :return: Table name, SQL statement used to load the file + """ + import os + + logger.debug(f"Loading {s3_path} into duckdb") + + if table_name is None: + # Get the file name from the s3 path + file_name = s3_path.split("/")[-1] + # Get the file name without extension from the s3 path + table_name, extension = os.path.splitext(file_name) + # If the table_name isn't a valid SQL identifier, we'll need to use something else + table_name = table_name.replace("-", "_").replace(".", "_").replace(" ", "_").replace("/", "_") + + select_statement = f"SELECT * FROM read_csv('{s3_path}'" + if delimiter is not None: + select_statement += f", delim='{delimiter}')" + else: + select_statement += ")" + + create_statement = f"CREATE OR REPLACE TABLE '{table_name}' AS {select_statement};" + self.run_duckdb_query(create_statement) + + logger.debug(f"Loaded CSV {s3_path} into duckdb as {table_name}") + # self.run_duckdb_query(f"SELECT * from {table_name};") + return table_name, create_statement + + def export_table_as(self, table_name: str, format: Optional[str] = "PARQUET", path: Optional[str] = None) -> str: + """Save a table to a desired format + The function will use the default format as parquet + If the path is provided, the table will be exported to that path, example s3 + + :param table_name: Table to export + :param format: Format to export to + :param path: Path to export to + :return: None + """ + if format is None: + format = "PARQUET" + + logger.debug(f"Exporting Table {table_name} as {format.upper()} in the path {path}") + # self.run_duckdb_query(f"SELECT * from {table_name};") + if path is None: + path = f"{table_name}.{format}" + else: + path = f"{path}/{table_name}.{format}" + export_statement = f"COPY (SELECT * FROM {table_name}) TO '{path}' (FORMAT {format.upper()});" + result = self.run_duckdb_query(export_statement) + logger.debug(f"Exported {table_name} to {path}/{table_name}") + + return result + + def create_fts_index(self, table_name: str, unique_key: str, input_values: list[str]) -> str: + """Create a full text search index on a table + + :param table_name: Table to create the index on + :param unique_key: Unique key to use + :param input_values: Values to index + :return: None + """ + logger.debug(f"Creating FTS index on {table_name} for {input_values}") + self.run_duckdb_query("INSTALL fts;") + logger.debug("Installed FTS extension") + self.run_duckdb_query("LOAD fts;") + logger.debug("Loaded FTS extension") + + create_fts_index_statement = f"PRAGMA create_fts_index('{table_name}', '{unique_key}', '{input_values}');" + logger.debug(f"Running {create_fts_index_statement}") + result = self.run_duckdb_query(create_fts_index_statement) + logger.debug(f"Created FTS index on {table_name} for {input_values}") + + return result + + def full_text_search(self, table_name: str, unique_key: str, search_text: str) -> str: + """Full text Search in a table column for a specific text/keyword + + :param table_name: Table to search + :param unique_key: Unique key to use + :param search_text: Text to search + :return: None + """ + logger.debug(f"Running full_text_search for {search_text} in {table_name}") + search_text_statement = f"""SELECT fts_main_corpus.match_bm25({unique_key}, '{search_text}') AS score,* + FROM {table_name} + WHERE score IS NOT NULL + ORDER BY score;""" + + logger.debug(f"Running {search_text_statement}") + result = self.run_duckdb_query(search_text_statement) + logger.debug(f"Search results for {search_text} in {table_name}") + + return result diff --git a/phi/llm/agent/email.py b/phi/llm/agent/email.py new file mode 100644 index 000000000..1df6a7201 --- /dev/null +++ b/phi/llm/agent/email.py @@ -0,0 +1,59 @@ +from typing import Optional + +from phi.llm.agent.base import BaseAgent +from phi.utils.log import logger + + +class EmailAgent(BaseAgent): + def __init__( + self, + receiver_email: Optional[str] = None, + sender_name: Optional[str] = None, + sender_email: Optional[str] = None, + sender_passkey: Optional[str] = None, + ): + super().__init__(name="email_agent") + self.receiver_email: Optional[str] = receiver_email + self.sender_name: Optional[str] = sender_name + self.sender_email: Optional[str] = sender_email + self.sender_passkey: Optional[str] = sender_passkey + self.register(self.email_user) + + def email_user(self, subject: str, body: str) -> str: + """Emails the user with the given subject and body. + + :param subject: The subject of the email. + :param body: The body of the email. + :return: "success" if the email was sent successfully, "error: [error message]" otherwise. + """ + try: + import smtplib + from email.message import EmailMessage + except ImportError: + logger.error("`smtplib` not installed") + raise + + if not self.receiver_email: + return "error: No receiver email provided" + if not self.sender_name: + return "error: No sender name provided" + if not self.sender_email: + return "error: No sender email provided" + if not self.sender_passkey: + return "error: No sender passkey provided" + + msg = EmailMessage() + msg["Subject"] = subject + msg["From"] = f"{self.sender_name} <{self.sender_email}>" + msg["To"] = self.receiver_email + msg.set_content(body) + + logger.info(f"Sending Email to {self.receiver_email}") + try: + with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp: + smtp.login(self.sender_email, self.sender_passkey) + smtp.send_message(msg) + except Exception as e: + logger.error(f"Error sending email: {e}") + return f"error: {e}" + return "email sent successfully" diff --git a/phi/llm/agent/google.py b/phi/llm/agent/google.py new file mode 100644 index 000000000..79f3ca7f3 --- /dev/null +++ b/phi/llm/agent/google.py @@ -0,0 +1,18 @@ +from phi.llm.agent.base import BaseAgent +from phi.utils.log import logger + + +class GoogleAgent(BaseAgent): + def __init__(self): + super().__init__(name="google_agent") + self.register(self.get_result_from_google) + + def get_result_from_google(self, query: str) -> str: + """Gets the result for a query from Google. + Use this function to find an answer when not available in the knowledge base. + + :param query: The query to search for. + :return: The result from Google. + """ + logger.info(f"Searching google for: {query}") + return "Sorry, this capability is not available yet." diff --git a/phi/llm/agent/phi.py b/phi/llm/agent/phi.py new file mode 100644 index 000000000..872e4d6ea --- /dev/null +++ b/phi/llm/agent/phi.py @@ -0,0 +1,116 @@ +import uuid +from typing import Optional + +from phi.llm.agent.base import BaseAgent +from phi.utils.log import logger + + +class PhiAgent(BaseAgent): + def __init__(self): + super().__init__(name="phi_agent") + self.register(self.create_new_app) + self.register(self.start_user_workspace) + self.register(self.validate_phi_is_ready) + + def validate_phi_is_ready(self) -> bool: + """Validates that Phi is ready to run commands. + + :return: True if Phi is ready, False otherwise. + """ + # Check if docker is running + return True + + def create_new_app(self, template: str, workspace_name: str) -> str: + """Creates a new phidata workspace for a given application template. + Use this function when the user wants to create a new "llm-app", "api-app", "django-app", or "streamlit-app". + Remember to provide a name for the new workspace. + You can use the format: "template-name" + name of an interesting person (lowercase, no spaces). + + :param template: (required) The template to use for the new application. + One of: llm-app, api-app, django-app, streamlit-app + :param workspace_name: (required) The name of the workspace to create for the new application. + :return: Status of the function or next steps. + """ + from phi.workspace.operator import create_workspace, TEMPLATE_TO_NAME_MAP, WorkspaceStarterTemplate + + ws_template: Optional[WorkspaceStarterTemplate] = None + if template.lower() in WorkspaceStarterTemplate.__members__.values(): + ws_template = WorkspaceStarterTemplate(template) + + if ws_template is None: + return f"Error: Invalid template: {template}, must be one of: llm-app, api-app, django-app, streamlit-app" + + ws_dir_name: Optional[str] = workspace_name + if ws_dir_name is None: + # Get default_ws_name from template + default_ws_name: Optional[str] = TEMPLATE_TO_NAME_MAP.get(ws_template) + # Add a 2 digit random suffix to the default_ws_name + random_suffix = str(uuid.uuid4())[:2] + default_ws_name = f"{default_ws_name}-{random_suffix}" + + return ( + f"Ask the user for a name for the app directory with the default value: {default_ws_name}." + f"Ask the user to input YES or NO to use the default value." + ) + # # Ask user for workspace name if not provided + # ws_dir_name = Prompt.ask("Please provide a name for the app", default=default_ws_name, console=console) + + logger.info(f"Creating: {template} at {ws_dir_name}") + try: + create_successful = create_workspace(name=ws_dir_name, template=ws_template.value) + if create_successful: + return ( + f"Successfully created a {ws_template.value} at {ws_dir_name}. " + f"Ask the user if they want to start the app now." + ) + else: + return f"Error: Failed to create {template}" + except Exception as e: + return f"Error: {e}" + + def start_user_workspace(self, workspace_name: Optional[str] = None) -> str: + """Starts the workspace for a user. Use this function when the user wants to start a given workspace. + If the workspace name is not provided, the function will start the active workspace. + Otherwise, it will start the workspace with the given name. + + :param workspace_name: The name of the workspace to start + :return: Status of the function or next steps. + """ + from phi.cli.config import PhiCliConfig + from phi.infra.type import InfraType + from phi.workspace.config import WorkspaceConfig + from phi.workspace.operator import start_workspace + + phi_config: Optional[PhiCliConfig] = PhiCliConfig.from_saved_config() + if not phi_config: + return "Error: Phi not initialized. Please run `phi ai` again" + + workspace_config_to_start: Optional[WorkspaceConfig] = None + active_ws_config: Optional[WorkspaceConfig] = phi_config.get_active_ws_config() + + if workspace_name is None: + if active_ws_config is None: + return "Error: No active workspace found. Please create a workspace first." + workspace_config_to_start = active_ws_config + else: + workspace_config_by_name: Optional[WorkspaceConfig] = phi_config.get_ws_config_by_dir_name(workspace_name) + if workspace_config_by_name is None: + return f"Error: Could not find a workspace with name: {workspace_name}" + workspace_config_to_start = workspace_config_by_name + + # Set the active workspace to the workspace to start + if active_ws_config is not None and active_ws_config.ws_root_path != workspace_config_by_name.ws_root_path: + phi_config.set_active_ws_dir(workspace_config_by_name.ws_root_path) + active_ws_config = workspace_config_by_name + + try: + start_workspace( + phi_config=phi_config, + ws_config=workspace_config_to_start, + target_env="dev", + target_infra=InfraType.docker, + auto_confirm=True, + ) + return f"Successfully started workspace: {workspace_config_to_start.ws_root_path.stem}" + except Exception as e: + return f"Error: {e}" diff --git a/phi/llm/agent/pubmed.py b/phi/llm/agent/pubmed.py new file mode 100644 index 000000000..15c04e97b --- /dev/null +++ b/phi/llm/agent/pubmed.py @@ -0,0 +1,19 @@ +from phi.llm.agent.base import BaseAgent +from phi.utils.log import logger + + +class PubMedAgent(BaseAgent): + def __init__(self): + super().__init__(name="pubmed_agent") + self.register(self.get_articles_from_pubmed) + + def get_articles_from_pubmed(self, query: str, num_articles: int = 2) -> str: + """Gets the abstract for articles related to a query from PubMed: a database of biomedical literature + Use this function to find information about a medical concept when not available in the knowledge base or Google + + :param query: The query to get related articles for. + :param num_articles: The number of articles to return. + :return: JSON string containing the articles + """ + logger.info(f"Searching Pubmed for: {query}") + return "Sorry, this capability is not available yet." diff --git a/phi/llm/agent/shell.py b/phi/llm/agent/shell.py new file mode 100644 index 000000000..4cb4dcc29 --- /dev/null +++ b/phi/llm/agent/shell.py @@ -0,0 +1,34 @@ +from typing import List + +from phi.llm.agent.base import BaseAgent +from phi.utils.log import logger + + +class ShellAgent(BaseAgent): + def __init__(self): + super().__init__(name="shell_agent") + self.register(self.run_shell_command) + + def run_shell_command(self, args: List[str], tail: int = 100) -> str: + """Runs a shell command and returns the output or error. + + :param args: The command to run as a list of strings. + :param tail: The number of lines to return from the output. + :return: The output of the command. + """ + logger.info(f"Running shell command: {args}") + + import subprocess + + try: + result = subprocess.run(args, capture_output=True, text=True) + logger.debug(f"Result: {result}") + logger.debug(f"Return code: {result.returncode}") + if result.returncode != 0: + return f"Error: {result.stderr}" + + # return only the last n lines of the output + return "\n".join(result.stdout.split("\n")[-tail:]) + except Exception as e: + logger.warning(f"Failed to run shell command: {e}") + return f"Error: {e}" diff --git a/phi/llm/agent/website.py b/phi/llm/agent/website.py new file mode 100644 index 000000000..5620b0393 --- /dev/null +++ b/phi/llm/agent/website.py @@ -0,0 +1,50 @@ +import json +from typing import List, Optional + +from phi.document import Document +from phi.knowledge.website import WebsiteKnowledgeBase +from phi.llm.agent.base import BaseAgent +from phi.utils.log import logger + + +class WebsiteAgent(BaseAgent): + def __init__(self, knowledge_base: Optional[WebsiteKnowledgeBase] = None): + super().__init__(name="website_agent") + self.knowledge_base: Optional[WebsiteKnowledgeBase] = knowledge_base + + if self.knowledge_base is not None and isinstance(self.knowledge_base, WebsiteKnowledgeBase): + self.register(self.add_website_to_knowledge_base) + else: + self.register(self.read_website) + + def add_website_to_knowledge_base(self, url: str) -> str: + """This function adds a websites content to the knowledge base. + NOTE: The website must start with https:// and should be a valid website. + + USE THIS FUNCTION TO GET INFORMATION ABOUT PRODUCTS FROM THE INTERNET. + + :param url: The url of the website to add. + :return: 'Success' if the website was added to the knowledge base. + """ + if self.knowledge_base is None: + return "Knowledge base not provided" + + logger.debug(f"Adding to knowledge base: {url}") + self.knowledge_base.urls.append(url) + logger.debug("Loading knowledge base.") + self.knowledge_base.load(recreate=False) + return "Success" + + def read_website(self, url: str) -> str: + """This function reads a website and returns the content. + + :param url: The url of the website to read. + :return: Relevant documents from the website. + """ + from phi.document.reader.website import WebsiteReader + + website = WebsiteReader() + + logger.debug(f"Reading website: {url}") + relevant_docs: List[Document] = website.read(url=url) + return json.dumps([doc.to_dict() for doc in relevant_docs]) diff --git a/phi/llm/agent/wikipedia.py b/phi/llm/agent/wikipedia.py new file mode 100644 index 000000000..ff2bb2377 --- /dev/null +++ b/phi/llm/agent/wikipedia.py @@ -0,0 +1,54 @@ +import json +from typing import List, Optional + +from phi.document import Document +from phi.knowledge.wikipedia import WikipediaKnowledgeBase +from phi.llm.agent.base import BaseAgent +from phi.utils.log import logger + + +class WikipediaAgent(BaseAgent): + def __init__(self, knowledge_base: Optional[WikipediaKnowledgeBase] = None): + super().__init__(name="wikipedia_agent") + self.knowledge_base: Optional[WikipediaKnowledgeBase] = knowledge_base + + if self.knowledge_base is not None and isinstance(self.knowledge_base, WikipediaKnowledgeBase): + self.register(self.search_wikipedia_and_update_knowledge_base) + else: + self.register(self.search_wikipedia) + + def search_wikipedia_and_update_knowledge_base(self, topic: str) -> str: + """This function searches wikipedia for a topic, adds the results to the knowledge base and returns them. + + USE THIS FUNCTION TO GET INFORMATION WHICH DOES NOT EXIST. + + :param topic: The topic to search Wikipedia and add to knowledge base. + :return: Relevant documents from Wikipedia knowledge base. + """ + + if self.knowledge_base is None: + return "Knowledge base not provided" + + logger.debug(f"Adding to knowledge base: {topic}") + self.knowledge_base.topics.append(topic) + logger.debug("Loading knowledge base.") + self.knowledge_base.load(recreate=False) + logger.debug(f"Searching knowledge base: {topic}") + relevant_docs: List[Document] = self.knowledge_base.search(query=topic) + return json.dumps([doc.to_dict() for doc in relevant_docs]) + + def search_wikipedia(self, query: str) -> str: + """Searches Wikipedia for a query. + + :param query: The query to search for. + :return: Relevant documents from wikipedia. + """ + try: + import wikipedia # noqa: F401 + except ImportError: + raise ImportError( + "The `wikipedia` package is not installed. " "Please install it via `pip install wikipedia`." + ) + + logger.info(f"Searching wikipedia for: {query}") + return json.dumps(Document(name=query, content=wikipedia.summary(query)).to_dict()) diff --git a/phi/llm/aws/bedrock.py b/phi/llm/aws/bedrock.py index 5c65422ae..83a2b63d0 100644 --- a/phi/llm/aws/bedrock.py +++ b/phi/llm/aws/bedrock.py @@ -3,7 +3,7 @@ from phi.aws.api_client import AwsApiClient from phi.llm.base import LLM -from phi.llm.schemas import Message +from phi.llm.message import Message from phi.utils.log import logger from phi.utils.timer import Timer diff --git a/phi/llm/base.py b/phi/llm/base.py index a3d217263..ee69c3951 100644 --- a/phi/llm/base.py +++ b/phi/llm/base.py @@ -2,9 +2,9 @@ from pydantic import BaseModel, ConfigDict -from phi.llm.schemas import Message +from phi.agent import Agent +from phi.llm.message import Message from phi.tool.tool import Tool -from phi.tool.registry import ToolRegistry from phi.tool.function import Function, FunctionCall from phi.utils.log import logger @@ -84,7 +84,7 @@ def get_tools_for_api(self) -> Optional[List[Dict[str, Any]]]: tools_for_api.append(tool) return tools_for_api - def add_tool(self, tool: Union[Tool, Dict, Callable, ToolRegistry]) -> None: + def add_tool(self, tool: Union[Tool, Dict, Callable, Agent]) -> None: if self.tools is None: self.tools = [] @@ -94,11 +94,11 @@ def add_tool(self, tool: Union[Tool, Dict, Callable, ToolRegistry]) -> None: logger.debug(f"Added tool {tool} to LLM.") # If the tool is a Callable or ToolRegistry, add its functions to the LLM - if callable(tool) or isinstance(tool, ToolRegistry): + if callable(tool) or isinstance(tool, Agent): if self.functions is None: self.functions = {} - if isinstance(tool, ToolRegistry): + if isinstance(tool, Agent): self.functions.update(tool.functions) for func in tool.functions.values(): self.tools.append({"type": "function", "function": func.to_dict()}) diff --git a/phi/llm/schemas.py b/phi/llm/message.py similarity index 90% rename from phi/llm/schemas.py rename to phi/llm/message.py index 014d7920c..0b6d1a0a7 100644 --- a/phi/llm/schemas.py +++ b/phi/llm/message.py @@ -20,11 +20,12 @@ class Message(BaseModel): tool_call_id: Optional[str] = None # The tool calls generated by the model, such as function calls. tool_calls: Optional[List[Dict[str, Any]]] = None - # DEPRECATED: The name and arguments of a function that should be called, as generated by the model. - function_call: Optional[Dict[str, Any]] = None # Metrics for the message, tokes + the time it took to generate the response. metrics: Dict[str, Any] = {} + # DEPRECATED: The name and arguments of a function that should be called, as generated by the model. + function_call: Optional[Dict[str, Any]] = None + def get_content_string(self) -> str: """Returns the content as a string.""" if isinstance(self.content, str): @@ -63,14 +64,3 @@ def log(self, level: Optional[str] = None): _logger(f"{self.content}") else: _logger(f"{self.role.upper()}: {self.content or self.function_call}") - - -class References(BaseModel): - """Model for LLM references""" - - # The question asked by the user. - query: str - # The references from the vector database. - references: str - # Performance in seconds. - time: Optional[float] = None diff --git a/phi/llm/openai/chat.py b/phi/llm/openai/chat.py index f91e226d3..4ef95ab1b 100644 --- a/phi/llm/openai/chat.py +++ b/phi/llm/openai/chat.py @@ -1,7 +1,7 @@ from typing import Optional, List, Iterator, Dict, Any, Union, Tuple from phi.llm.base import LLM -from phi.llm.schemas import Message +from phi.llm.message import Message from phi.tool.function import FunctionCall from phi.utils.env import get_from_env from phi.utils.log import logger diff --git a/phi/llm/references.py b/phi/llm/references.py new file mode 100644 index 000000000..0fa97a17b --- /dev/null +++ b/phi/llm/references.py @@ -0,0 +1,13 @@ +from typing import Optional +from pydantic import BaseModel + + +class References(BaseModel): + """Model for LLM references""" + + # The question asked by the user. + query: str + # The references from the vector database. + references: str + # Performance in seconds. + time: Optional[float] = None diff --git a/phi/llm/task/llm_task.py b/phi/llm/task/llm_task.py index e0e895474..8425fabba 100644 --- a/phi/llm/task/llm_task.py +++ b/phi/llm/task/llm_task.py @@ -3,14 +3,15 @@ from pydantic import BaseModel, ConfigDict, Field +from phi.agent import Agent from phi.document import Document from phi.knowledge.base import KnowledgeBase from phi.llm.base import LLM from phi.llm.openai import OpenAIChat -from phi.llm.schemas import Message, References +from phi.llm.message import Message +from phi.llm.references import References +from phi.llm.task.memory import TaskMemory from phi.tool.tool import Tool -from phi.tool.registry import ToolRegistry -from phi.llm.task.memory.base import TaskMemory from phi.utils.format_str import remove_indent from phi.utils.log import logger from phi.utils.timer import Timer @@ -53,7 +54,7 @@ class LLMTask(BaseModel): # A list of tools provided to the LLM. # Currently, only functions are supported as a tool. # Use this to provide a list of functions the model may generate JSON inputs for. - tools: Optional[List[Union[Tool, Dict, Callable, ToolRegistry]]] = None + tools: Optional[List[Union[Tool, Dict, Callable, Agent]]] = None # Controls which (if any) function is called by the model. # "none" means the model will not call a function and instead generates a message. # "auto" means the model can pick between generating a message or calling a function. @@ -62,6 +63,11 @@ class LLMTask(BaseModel): # "none" is the default when no functions are present. "auto" is the default if functions are present. tool_choice: Optional[Union[str, Dict[str, Any]]] = None + # -*- Agents + # A list of agents provided to the LLM + # These are added to the tools list + agents: Optional[List[Agent]] = None + # # -*- Prompt Settings # @@ -114,6 +120,14 @@ def add_tools_to_llm(self) -> None: if self.llm is None: return + if self.tools is not None: + for tool in self.tools: + self.llm.add_tool(tool) + + if self.agents is not None: + for agent in self.agents: + self.llm.add_tool(agent) + if self.function_calls and self.default_functions: default_func_list: List[Callable] = [ self.get_last_n_chats, diff --git a/phi/llm/task/memory/base.py b/phi/llm/task/memory/base.py index 15e5e2423..30f4c6a50 100644 --- a/phi/llm/task/memory/base.py +++ b/phi/llm/task/memory/base.py @@ -2,7 +2,8 @@ from pydantic import BaseModel -from phi.llm.schemas import Message, References +from phi.llm.message import Message +from phi.llm.references import References class TaskMemory(BaseModel): From 6e04990a362cfc00d9549809a690dcbd317b35b7 Mon Sep 17 00:00:00 2001 From: Ashpreet Bedi Date: Fri, 10 Nov 2023 14:32:06 +0000 Subject: [PATCH 4/5] v2.0.36 --- phi/llm/agent/arxiv.py | 53 ------ phi/llm/agent/base.py | 27 --- phi/llm/agent/duckdb.py | 339 ------------------------------------- phi/llm/agent/email.py | 59 ------- phi/llm/agent/google.py | 18 -- phi/llm/agent/phi.py | 116 ------------- phi/llm/agent/pubmed.py | 19 --- phi/llm/agent/shell.py | 34 ---- phi/llm/agent/website.py | 52 +----- phi/llm/agent/wikipedia.py | 54 ------ 10 files changed, 2 insertions(+), 769 deletions(-) delete mode 100644 phi/llm/agent/arxiv.py delete mode 100644 phi/llm/agent/base.py delete mode 100644 phi/llm/agent/duckdb.py delete mode 100644 phi/llm/agent/email.py delete mode 100644 phi/llm/agent/google.py delete mode 100644 phi/llm/agent/phi.py delete mode 100644 phi/llm/agent/pubmed.py delete mode 100644 phi/llm/agent/shell.py delete mode 100644 phi/llm/agent/wikipedia.py diff --git a/phi/llm/agent/arxiv.py b/phi/llm/agent/arxiv.py deleted file mode 100644 index e7b2660bb..000000000 --- a/phi/llm/agent/arxiv.py +++ /dev/null @@ -1,53 +0,0 @@ -import json -from typing import List, Optional - -from phi.document import Document -from phi.knowledge.arxiv import ArxivKnowledgeBase -from phi.llm.agent.base import BaseAgent -from phi.utils.log import logger - - -class ArxivAgent(BaseAgent): - def __init__(self, knowledge_base: Optional[ArxivKnowledgeBase] = None): - super().__init__(name="arxiv_agent") - self.knowledge_base: Optional[ArxivKnowledgeBase] = knowledge_base - - if self.knowledge_base is not None and isinstance(self.knowledge_base, ArxivKnowledgeBase): - self.register(self.search_arxiv_and_update_knowledge_base) - else: - self.register(self.search_arxiv) - - def search_arxiv_and_update_knowledge_base(self, topic: str) -> str: - """This function searches arXiv for a topic, adds the results to the knowledge base and returns them. - - USE THIS FUNCTION TO GET INFORMATION WHICH DOES NOT EXIST. - - :param topic: The topic to search arXiv and add to knowledge base. - :return: Relevant documents from arXiv knowledge base. - """ - if self.knowledge_base is None: - return "Knowledge base not provided" - - logger.debug(f"Adding to knowledge base: {topic}") - self.knowledge_base.queries.append(topic) - logger.debug("Loading knowledge base.") - self.knowledge_base.load(recreate=False) - logger.debug(f"Searching knowledge base: {topic}") - relevant_docs: List[Document] = self.knowledge_base.search(query=topic) - return json.dumps([doc.to_dict() for doc in relevant_docs]) - - def search_arxiv(self, query: str, max_results: int = 5) -> str: - """ - Searches arXiv for a query. - - :param query: The query to search for. - :param max_results: The maximum number of results to return. - :return: Relevant documents from arXiv. - """ - from phi.document.reader.arxiv import ArxivReader - - arxiv = ArxivReader(max_results=max_results) - - logger.debug(f"Searching arxiv for: {query}") - relevant_docs: List[Document] = arxiv.read(query=query) - return json.dumps([doc.to_dict() for doc in relevant_docs]) diff --git a/phi/llm/agent/base.py b/phi/llm/agent/base.py deleted file mode 100644 index 427ae4bb7..000000000 --- a/phi/llm/agent/base.py +++ /dev/null @@ -1,27 +0,0 @@ -from collections import OrderedDict -from typing import Callable, Dict - -from phi.tool.function import Function -from phi.utils.log import logger - - -class BaseAgent: - def __init__(self, name: str = "base_agent"): - self.name: str = name - self.functions: Dict[str, Function] = OrderedDict() - - def register(self, function: Callable): - try: - f = Function.from_callable(function) - self.functions[f.name] = f - logger.debug(f"Function: {f.name} registered with {self.name}") - logger.debug(f"Json Schema: {f.to_dict()}") - except Exception as e: - logger.warning(f"Failed to create Function for: {function.__name__}") - raise e - - def __repr__(self): - return f"<{self.__class__.__name__} name={self.name} functions={list(self.functions.keys())}>" - - def __str__(self): - return self.__repr__() diff --git a/phi/llm/agent/duckdb.py b/phi/llm/agent/duckdb.py deleted file mode 100644 index 9f0abe157..000000000 --- a/phi/llm/agent/duckdb.py +++ /dev/null @@ -1,339 +0,0 @@ -from typing import Optional, Tuple - -from phi.llm.agent.base import BaseAgent -from phi.utils.log import logger - -try: - import duckdb -except ImportError: - raise ImportError("`duckdb` not installed. Please install it using `pip install duckdb`.") - - -class DuckDbAgent(BaseAgent): - def __init__( - self, - db_path: str = ":memory:", - s3_region: str = "us-east-1", - duckdb_connection: Optional[duckdb.DuckDBPyConnection] = None, - ): - super().__init__(name="duckdb_registry") - - self.db_path: str = db_path - self.s3_region: str = s3_region - self._duckdb_connection: Optional[duckdb.DuckDBPyConnection] = duckdb_connection - - self.register(self.run_duckdb_query) - self.register(self.show_tables) - self.register(self.describe_table) - self.register(self.inspect_query) - self.register(self.describe_table_or_view) - self.register(self.export_table_as) - self.register(self.summarize_table) - self.register(self.create_fts_index) - self.register(self.full_text_search) - - @property - def duckdb_connection(self) -> duckdb.DuckDBPyConnection: - """ - Returns the duckdb connection - - :return duckdb.DuckDBPyConnection: duckdb connection - """ - if self._duckdb_connection is None: - self._duckdb_connection = duckdb.connect(self.db_path) - try: - self._duckdb_connection.sql("INSTALL httpfs;") - self._duckdb_connection.sql("LOAD httpfs;") - self._duckdb_connection.sql(f"SET s3_region='{self.s3_region}';") - except Exception as e: - logger.exception(e) - logger.warning("Failed to install httpfs extension. Only local files will be supported") - - return self._duckdb_connection - - def run_duckdb_query(self, query: str) -> str: - """Function to run SQL queries against a duckdb database - - :param query: SQL query to run - :return: Result of the query - """ - - # -*- Format the SQL Query - # Remove backticks - formatted_sql = query.replace("`", "") - # If there are multiple statements, only run the first one - formatted_sql = formatted_sql.split(";")[0] - - try: - logger.debug(f"Running query: {formatted_sql}") - - query_result = self.duckdb_connection.sql(formatted_sql) - result_output = "No output" - if query_result is not None: - try: - results_as_python_objects = query_result.fetchall() - result_rows = [] - for row in results_as_python_objects: - if len(row) == 1: - result_rows.append(str(row[0])) - else: - result_rows.append(",".join(str(x) for x in row)) - - result_data = "\n".join(result_rows) - result_output = ",".join(query_result.columns) + "\n" + result_data - except AttributeError: - result_output = str(query_result) - - logger.debug(f"Query result: {result_output}") - return result_output - except duckdb.ProgrammingError as e: - return str(e) - except duckdb.Error as e: - return str(e) - except Exception as e: - return str(e) - - def show_tables(self) -> str: - """Function to show tables in the database - - :return: List of tables in the database - """ - stmt = "SHOW TABLES;" - tables = self.run_duckdb_query(stmt) - logger.debug(f"Tables: {tables}") - return tables - - def describe_table(self, table: str) -> str: - """Function to describe a table - - :param table: Table to describe - :return: Description of the table - """ - stmt = f"DESCRIBE {table};" - table_description = self.run_duckdb_query(stmt) - - logger.debug(f"Table description: {table_description}") - return f"{table}\n{table_description}" - - def summarize_table(self, table: str) -> str: - """Function to summarize the contents of a table - - :param table: Table to describe - :return: Description of the table - """ - stmt = f"SUMMARIZE SELECT * FROM {table};" - table_description = self.run_duckdb_query(stmt) - - logger.debug(f"Table description: {table_description}") - return f"{table}\n{table_description}" - - def inspect_query(self, query: str) -> str: - """Function to inspect a query and return the query plan. Always inspect your query before running them. - - :param query: Query to inspect - :return: Qeury plan - """ - stmt = f"explain {query};" - explain_plan = self.run_duckdb_query(stmt) - - logger.debug(f"Explain plan: {explain_plan}") - return explain_plan - - def describe_table_or_view(self, table: str): - """Function to describe a table or view - - :param table: Table or view to describe - :return: Description of the table or view - """ - stmt = f"select column_name, data_type from information_schema.columns where table_name='{table}';" - table_description = self.run_duckdb_query(stmt) - - logger.debug(f"Table description: {table_description}") - return f"{table}\n{table_description}" - - def load_local_path_to_table(self, path: str, table_name: Optional[str] = None) -> Tuple[str, str]: - """Load a local file into duckdb - - :param path: Path to load - :param table_name: Optional table name to use - :return: Table name, SQL statement used to load the file - """ - import os - - logger.debug(f"Loading {path} into duckdb") - - if table_name is None: - # Get the file name from the s3 path - file_name = path.split("/")[-1] - # Get the file name without extension from the s3 path - table_name, extension = os.path.splitext(file_name) - # If the table_name isn't a valid SQL identifier, we'll need to use something else - table_name = table_name.replace("-", "_").replace(".", "_").replace(" ", "_").replace("/", "_") - - create_statement = f"CREATE OR REPLACE TABLE '{table_name}' AS SELECT * FROM '{path}';" - self.run_duckdb_query(create_statement) - - logger.debug(f"Loaded {path} into duckdb as {table_name}") - # self.run_duckdb_query(f"SELECT * from {table_name};") - return table_name, create_statement - - def load_local_csv_to_table( - self, path: str, table_name: Optional[str] = None, delimiter: Optional[str] = None - ) -> Tuple[str, str]: - """Load a local CSV file into duckdb - - :param path: Path to load - :param table_name: Optional table name to use - :param delimiter: Optional delimiter to use - :return: Table name, SQL statement used to load the file - """ - import os - - logger.debug(f"Loading {path} into duckdb") - - if table_name is None: - # Get the file name from the s3 path - file_name = path.split("/")[-1] - # Get the file name without extension from the s3 path - table_name, extension = os.path.splitext(file_name) - # If the table_name isn't a valid SQL identifier, we'll need to use something else - table_name = table_name.replace("-", "_").replace(".", "_").replace(" ", "_").replace("/", "_") - - select_statement = f"SELECT * FROM read_csv('{path}'" - if delimiter is not None: - select_statement += f", delim='{delimiter}')" - else: - select_statement += ")" - - create_statement = f"CREATE OR REPLACE TABLE '{table_name}' AS {select_statement};" - self.run_duckdb_query(create_statement) - - logger.debug(f"Loaded CSV {path} into duckdb as {table_name}") - # self.run_duckdb_query(f"SELECT * from {table_name};") - return table_name, create_statement - - def load_s3_path_to_table(self, s3_path: str, table_name: Optional[str] = None) -> Tuple[str, str]: - """Load a file from S3 into duckdb - - :param s3_path: S3 path to load - :param table_name: Optional table name to use - :return: Table name, SQL statement used to load the file - """ - import os - - logger.debug(f"Loading {s3_path} into duckdb") - - if table_name is None: - # Get the file name from the s3 path - file_name = s3_path.split("/")[-1] - # Get the file name without extension from the s3 path - table_name, extension = os.path.splitext(file_name) - # If the table_name isn't a valid SQL identifier, we'll need to use something else - table_name = table_name.replace("-", "_").replace(".", "_").replace(" ", "_").replace("/", "_") - - create_statement = f"CREATE OR REPLACE TABLE '{table_name}' AS SELECT * FROM '{s3_path}';" - self.run_duckdb_query(create_statement) - - logger.debug(f"Loaded {s3_path} into duckdb as {table_name}") - # self.run_duckdb_query(f"SELECT * from {table_name};") - return table_name, create_statement - - def load_s3_csv_to_table( - self, s3_path: str, table_name: Optional[str] = None, delimiter: Optional[str] = None - ) -> Tuple[str, str]: - """Load a CSV file from S3 into duckdb - - :param s3_path: S3 path to load - :param table_name: Optional table name to use - :return: Table name, SQL statement used to load the file - """ - import os - - logger.debug(f"Loading {s3_path} into duckdb") - - if table_name is None: - # Get the file name from the s3 path - file_name = s3_path.split("/")[-1] - # Get the file name without extension from the s3 path - table_name, extension = os.path.splitext(file_name) - # If the table_name isn't a valid SQL identifier, we'll need to use something else - table_name = table_name.replace("-", "_").replace(".", "_").replace(" ", "_").replace("/", "_") - - select_statement = f"SELECT * FROM read_csv('{s3_path}'" - if delimiter is not None: - select_statement += f", delim='{delimiter}')" - else: - select_statement += ")" - - create_statement = f"CREATE OR REPLACE TABLE '{table_name}' AS {select_statement};" - self.run_duckdb_query(create_statement) - - logger.debug(f"Loaded CSV {s3_path} into duckdb as {table_name}") - # self.run_duckdb_query(f"SELECT * from {table_name};") - return table_name, create_statement - - def export_table_as(self, table_name: str, format: Optional[str] = "PARQUET", path: Optional[str] = None) -> str: - """Save a table to a desired format - The function will use the default format as parquet - If the path is provided, the table will be exported to that path, example s3 - - :param table_name: Table to export - :param format: Format to export to - :param path: Path to export to - :return: None - """ - if format is None: - format = "PARQUET" - - logger.debug(f"Exporting Table {table_name} as {format.upper()} in the path {path}") - # self.run_duckdb_query(f"SELECT * from {table_name};") - if path is None: - path = f"{table_name}.{format}" - else: - path = f"{path}/{table_name}.{format}" - export_statement = f"COPY (SELECT * FROM {table_name}) TO '{path}' (FORMAT {format.upper()});" - result = self.run_duckdb_query(export_statement) - logger.debug(f"Exported {table_name} to {path}/{table_name}") - - return result - - def create_fts_index(self, table_name: str, unique_key: str, input_values: list[str]) -> str: - """Create a full text search index on a table - - :param table_name: Table to create the index on - :param unique_key: Unique key to use - :param input_values: Values to index - :return: None - """ - logger.debug(f"Creating FTS index on {table_name} for {input_values}") - self.run_duckdb_query("INSTALL fts;") - logger.debug("Installed FTS extension") - self.run_duckdb_query("LOAD fts;") - logger.debug("Loaded FTS extension") - - create_fts_index_statement = f"PRAGMA create_fts_index('{table_name}', '{unique_key}', '{input_values}');" - logger.debug(f"Running {create_fts_index_statement}") - result = self.run_duckdb_query(create_fts_index_statement) - logger.debug(f"Created FTS index on {table_name} for {input_values}") - - return result - - def full_text_search(self, table_name: str, unique_key: str, search_text: str) -> str: - """Full text Search in a table column for a specific text/keyword - - :param table_name: Table to search - :param unique_key: Unique key to use - :param search_text: Text to search - :return: None - """ - logger.debug(f"Running full_text_search for {search_text} in {table_name}") - search_text_statement = f"""SELECT fts_main_corpus.match_bm25({unique_key}, '{search_text}') AS score,* - FROM {table_name} - WHERE score IS NOT NULL - ORDER BY score;""" - - logger.debug(f"Running {search_text_statement}") - result = self.run_duckdb_query(search_text_statement) - logger.debug(f"Search results for {search_text} in {table_name}") - - return result diff --git a/phi/llm/agent/email.py b/phi/llm/agent/email.py deleted file mode 100644 index 1df6a7201..000000000 --- a/phi/llm/agent/email.py +++ /dev/null @@ -1,59 +0,0 @@ -from typing import Optional - -from phi.llm.agent.base import BaseAgent -from phi.utils.log import logger - - -class EmailAgent(BaseAgent): - def __init__( - self, - receiver_email: Optional[str] = None, - sender_name: Optional[str] = None, - sender_email: Optional[str] = None, - sender_passkey: Optional[str] = None, - ): - super().__init__(name="email_agent") - self.receiver_email: Optional[str] = receiver_email - self.sender_name: Optional[str] = sender_name - self.sender_email: Optional[str] = sender_email - self.sender_passkey: Optional[str] = sender_passkey - self.register(self.email_user) - - def email_user(self, subject: str, body: str) -> str: - """Emails the user with the given subject and body. - - :param subject: The subject of the email. - :param body: The body of the email. - :return: "success" if the email was sent successfully, "error: [error message]" otherwise. - """ - try: - import smtplib - from email.message import EmailMessage - except ImportError: - logger.error("`smtplib` not installed") - raise - - if not self.receiver_email: - return "error: No receiver email provided" - if not self.sender_name: - return "error: No sender name provided" - if not self.sender_email: - return "error: No sender email provided" - if not self.sender_passkey: - return "error: No sender passkey provided" - - msg = EmailMessage() - msg["Subject"] = subject - msg["From"] = f"{self.sender_name} <{self.sender_email}>" - msg["To"] = self.receiver_email - msg.set_content(body) - - logger.info(f"Sending Email to {self.receiver_email}") - try: - with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp: - smtp.login(self.sender_email, self.sender_passkey) - smtp.send_message(msg) - except Exception as e: - logger.error(f"Error sending email: {e}") - return f"error: {e}" - return "email sent successfully" diff --git a/phi/llm/agent/google.py b/phi/llm/agent/google.py deleted file mode 100644 index 79f3ca7f3..000000000 --- a/phi/llm/agent/google.py +++ /dev/null @@ -1,18 +0,0 @@ -from phi.llm.agent.base import BaseAgent -from phi.utils.log import logger - - -class GoogleAgent(BaseAgent): - def __init__(self): - super().__init__(name="google_agent") - self.register(self.get_result_from_google) - - def get_result_from_google(self, query: str) -> str: - """Gets the result for a query from Google. - Use this function to find an answer when not available in the knowledge base. - - :param query: The query to search for. - :return: The result from Google. - """ - logger.info(f"Searching google for: {query}") - return "Sorry, this capability is not available yet." diff --git a/phi/llm/agent/phi.py b/phi/llm/agent/phi.py deleted file mode 100644 index 872e4d6ea..000000000 --- a/phi/llm/agent/phi.py +++ /dev/null @@ -1,116 +0,0 @@ -import uuid -from typing import Optional - -from phi.llm.agent.base import BaseAgent -from phi.utils.log import logger - - -class PhiAgent(BaseAgent): - def __init__(self): - super().__init__(name="phi_agent") - self.register(self.create_new_app) - self.register(self.start_user_workspace) - self.register(self.validate_phi_is_ready) - - def validate_phi_is_ready(self) -> bool: - """Validates that Phi is ready to run commands. - - :return: True if Phi is ready, False otherwise. - """ - # Check if docker is running - return True - - def create_new_app(self, template: str, workspace_name: str) -> str: - """Creates a new phidata workspace for a given application template. - Use this function when the user wants to create a new "llm-app", "api-app", "django-app", or "streamlit-app". - Remember to provide a name for the new workspace. - You can use the format: "template-name" + name of an interesting person (lowercase, no spaces). - - :param template: (required) The template to use for the new application. - One of: llm-app, api-app, django-app, streamlit-app - :param workspace_name: (required) The name of the workspace to create for the new application. - :return: Status of the function or next steps. - """ - from phi.workspace.operator import create_workspace, TEMPLATE_TO_NAME_MAP, WorkspaceStarterTemplate - - ws_template: Optional[WorkspaceStarterTemplate] = None - if template.lower() in WorkspaceStarterTemplate.__members__.values(): - ws_template = WorkspaceStarterTemplate(template) - - if ws_template is None: - return f"Error: Invalid template: {template}, must be one of: llm-app, api-app, django-app, streamlit-app" - - ws_dir_name: Optional[str] = workspace_name - if ws_dir_name is None: - # Get default_ws_name from template - default_ws_name: Optional[str] = TEMPLATE_TO_NAME_MAP.get(ws_template) - # Add a 2 digit random suffix to the default_ws_name - random_suffix = str(uuid.uuid4())[:2] - default_ws_name = f"{default_ws_name}-{random_suffix}" - - return ( - f"Ask the user for a name for the app directory with the default value: {default_ws_name}." - f"Ask the user to input YES or NO to use the default value." - ) - # # Ask user for workspace name if not provided - # ws_dir_name = Prompt.ask("Please provide a name for the app", default=default_ws_name, console=console) - - logger.info(f"Creating: {template} at {ws_dir_name}") - try: - create_successful = create_workspace(name=ws_dir_name, template=ws_template.value) - if create_successful: - return ( - f"Successfully created a {ws_template.value} at {ws_dir_name}. " - f"Ask the user if they want to start the app now." - ) - else: - return f"Error: Failed to create {template}" - except Exception as e: - return f"Error: {e}" - - def start_user_workspace(self, workspace_name: Optional[str] = None) -> str: - """Starts the workspace for a user. Use this function when the user wants to start a given workspace. - If the workspace name is not provided, the function will start the active workspace. - Otherwise, it will start the workspace with the given name. - - :param workspace_name: The name of the workspace to start - :return: Status of the function or next steps. - """ - from phi.cli.config import PhiCliConfig - from phi.infra.type import InfraType - from phi.workspace.config import WorkspaceConfig - from phi.workspace.operator import start_workspace - - phi_config: Optional[PhiCliConfig] = PhiCliConfig.from_saved_config() - if not phi_config: - return "Error: Phi not initialized. Please run `phi ai` again" - - workspace_config_to_start: Optional[WorkspaceConfig] = None - active_ws_config: Optional[WorkspaceConfig] = phi_config.get_active_ws_config() - - if workspace_name is None: - if active_ws_config is None: - return "Error: No active workspace found. Please create a workspace first." - workspace_config_to_start = active_ws_config - else: - workspace_config_by_name: Optional[WorkspaceConfig] = phi_config.get_ws_config_by_dir_name(workspace_name) - if workspace_config_by_name is None: - return f"Error: Could not find a workspace with name: {workspace_name}" - workspace_config_to_start = workspace_config_by_name - - # Set the active workspace to the workspace to start - if active_ws_config is not None and active_ws_config.ws_root_path != workspace_config_by_name.ws_root_path: - phi_config.set_active_ws_dir(workspace_config_by_name.ws_root_path) - active_ws_config = workspace_config_by_name - - try: - start_workspace( - phi_config=phi_config, - ws_config=workspace_config_to_start, - target_env="dev", - target_infra=InfraType.docker, - auto_confirm=True, - ) - return f"Successfully started workspace: {workspace_config_to_start.ws_root_path.stem}" - except Exception as e: - return f"Error: {e}" diff --git a/phi/llm/agent/pubmed.py b/phi/llm/agent/pubmed.py deleted file mode 100644 index 15c04e97b..000000000 --- a/phi/llm/agent/pubmed.py +++ /dev/null @@ -1,19 +0,0 @@ -from phi.llm.agent.base import BaseAgent -from phi.utils.log import logger - - -class PubMedAgent(BaseAgent): - def __init__(self): - super().__init__(name="pubmed_agent") - self.register(self.get_articles_from_pubmed) - - def get_articles_from_pubmed(self, query: str, num_articles: int = 2) -> str: - """Gets the abstract for articles related to a query from PubMed: a database of biomedical literature - Use this function to find information about a medical concept when not available in the knowledge base or Google - - :param query: The query to get related articles for. - :param num_articles: The number of articles to return. - :return: JSON string containing the articles - """ - logger.info(f"Searching Pubmed for: {query}") - return "Sorry, this capability is not available yet." diff --git a/phi/llm/agent/shell.py b/phi/llm/agent/shell.py deleted file mode 100644 index 4cb4dcc29..000000000 --- a/phi/llm/agent/shell.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import List - -from phi.llm.agent.base import BaseAgent -from phi.utils.log import logger - - -class ShellAgent(BaseAgent): - def __init__(self): - super().__init__(name="shell_agent") - self.register(self.run_shell_command) - - def run_shell_command(self, args: List[str], tail: int = 100) -> str: - """Runs a shell command and returns the output or error. - - :param args: The command to run as a list of strings. - :param tail: The number of lines to return from the output. - :return: The output of the command. - """ - logger.info(f"Running shell command: {args}") - - import subprocess - - try: - result = subprocess.run(args, capture_output=True, text=True) - logger.debug(f"Result: {result}") - logger.debug(f"Return code: {result.returncode}") - if result.returncode != 0: - return f"Error: {result.stderr}" - - # return only the last n lines of the output - return "\n".join(result.stdout.split("\n")[-tail:]) - except Exception as e: - logger.warning(f"Failed to run shell command: {e}") - return f"Error: {e}" diff --git a/phi/llm/agent/website.py b/phi/llm/agent/website.py index 5620b0393..652a77ff7 100644 --- a/phi/llm/agent/website.py +++ b/phi/llm/agent/website.py @@ -1,50 +1,2 @@ -import json -from typing import List, Optional - -from phi.document import Document -from phi.knowledge.website import WebsiteKnowledgeBase -from phi.llm.agent.base import BaseAgent -from phi.utils.log import logger - - -class WebsiteAgent(BaseAgent): - def __init__(self, knowledge_base: Optional[WebsiteKnowledgeBase] = None): - super().__init__(name="website_agent") - self.knowledge_base: Optional[WebsiteKnowledgeBase] = knowledge_base - - if self.knowledge_base is not None and isinstance(self.knowledge_base, WebsiteKnowledgeBase): - self.register(self.add_website_to_knowledge_base) - else: - self.register(self.read_website) - - def add_website_to_knowledge_base(self, url: str) -> str: - """This function adds a websites content to the knowledge base. - NOTE: The website must start with https:// and should be a valid website. - - USE THIS FUNCTION TO GET INFORMATION ABOUT PRODUCTS FROM THE INTERNET. - - :param url: The url of the website to add. - :return: 'Success' if the website was added to the knowledge base. - """ - if self.knowledge_base is None: - return "Knowledge base not provided" - - logger.debug(f"Adding to knowledge base: {url}") - self.knowledge_base.urls.append(url) - logger.debug("Loading knowledge base.") - self.knowledge_base.load(recreate=False) - return "Success" - - def read_website(self, url: str) -> str: - """This function reads a website and returns the content. - - :param url: The url of the website to read. - :return: Relevant documents from the website. - """ - from phi.document.reader.website import WebsiteReader - - website = WebsiteReader() - - logger.debug(f"Reading website: {url}") - relevant_docs: List[Document] = website.read(url=url) - return json.dumps([doc.to_dict() for doc in relevant_docs]) +# DEPRECATED: Use phi.agent.website instead +from phi.agent.website import WebsiteAgent, WebsiteKnowledgeBase # noqa: F401 diff --git a/phi/llm/agent/wikipedia.py b/phi/llm/agent/wikipedia.py deleted file mode 100644 index ff2bb2377..000000000 --- a/phi/llm/agent/wikipedia.py +++ /dev/null @@ -1,54 +0,0 @@ -import json -from typing import List, Optional - -from phi.document import Document -from phi.knowledge.wikipedia import WikipediaKnowledgeBase -from phi.llm.agent.base import BaseAgent -from phi.utils.log import logger - - -class WikipediaAgent(BaseAgent): - def __init__(self, knowledge_base: Optional[WikipediaKnowledgeBase] = None): - super().__init__(name="wikipedia_agent") - self.knowledge_base: Optional[WikipediaKnowledgeBase] = knowledge_base - - if self.knowledge_base is not None and isinstance(self.knowledge_base, WikipediaKnowledgeBase): - self.register(self.search_wikipedia_and_update_knowledge_base) - else: - self.register(self.search_wikipedia) - - def search_wikipedia_and_update_knowledge_base(self, topic: str) -> str: - """This function searches wikipedia for a topic, adds the results to the knowledge base and returns them. - - USE THIS FUNCTION TO GET INFORMATION WHICH DOES NOT EXIST. - - :param topic: The topic to search Wikipedia and add to knowledge base. - :return: Relevant documents from Wikipedia knowledge base. - """ - - if self.knowledge_base is None: - return "Knowledge base not provided" - - logger.debug(f"Adding to knowledge base: {topic}") - self.knowledge_base.topics.append(topic) - logger.debug("Loading knowledge base.") - self.knowledge_base.load(recreate=False) - logger.debug(f"Searching knowledge base: {topic}") - relevant_docs: List[Document] = self.knowledge_base.search(query=topic) - return json.dumps([doc.to_dict() for doc in relevant_docs]) - - def search_wikipedia(self, query: str) -> str: - """Searches Wikipedia for a query. - - :param query: The query to search for. - :return: Relevant documents from wikipedia. - """ - try: - import wikipedia # noqa: F401 - except ImportError: - raise ImportError( - "The `wikipedia` package is not installed. " "Please install it via `pip install wikipedia`." - ) - - logger.info(f"Searching wikipedia for: {query}") - return json.dumps(Document(name=query, content=wikipedia.summary(query)).to_dict()) From bc4627b30d5d94976710b9764ddc4919a55b42d2 Mon Sep 17 00:00:00 2001 From: Ashpreet Bedi Date: Fri, 10 Nov 2023 16:05:25 +0000 Subject: [PATCH 5/5] v2.0.36 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3a73217fe..adeed43aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "phidata" -version = "2.0.35" +version = "2.0.36" description = "AI Toolkit for Engineers" requires-python = ">=3.7" readme = "README.md"