diff --git a/docs/release-notes/breaking-changes.md b/docs/release-notes/breaking-changes.md index 3846186bf..42ede7467 100644 --- a/docs/release-notes/breaking-changes.md +++ b/docs/release-notes/breaking-changes.md @@ -3,6 +3,53 @@ > [!NOTE] > This section is for changes that are not yet released but will affect future releases. +## Starting with 0.9.1-rc105 + +### Configuration changes + +The following new App Configuration settings are required: + +|Name | Default value | Description | +|--- | --- | --- | +|`FoundationaLLM:PythonSDK:Logging:LogLevel:Azure` | `Warning` | Provides the default level of logging for Azure modules in the Python SDK. | + +### Agent workflow configuration changes + +Agent resource configuration files that have a `workflow` property now requires a `name` and `package_name` property. This is to support loading external workflows via plugins. For internal workflows, the `package_name` should be set to `FoundationaLLM`. Example below truncated for brevity. + +```json +{ + "workflow": { + "type": "langgraph-react-agent-workflow", + "name": "LangGraphReactAgent", + "package_name": "FoundationaLLM", + "workflow_host": "LangChain", + "graph_recursion_limit": 10, + "resource_object_ids": {} + } +} +``` + +A new `Workflow` resource must be added to the `FoundationaLLM.Agent` resource provider: + +```json +{ + "type": "external-agent-workflow", + "name": "ExternalAgentWorkflow", + "object_id": "/instances//providers/FoundationaLLM.Agent/workflows/ExternalAgentWorkflow", + "display_name": "ExternalAgentWorkflow", + "description": "External Agent workflow", + "cost_center": null, + "properties": null, + "created_on": "2024-11-13T18:12:07.0223039+00:00", + "updated_on": "0001-01-01T00:00:00+00:00", + "created_by": "dev@foundationaLLM.ai", + "updated_by": null, + "deleted": false, + "expiration_date": null +} +``` + ## Starting with 0.9.1-rc102 ### Configuration changes diff --git a/src/dotnet/Common/Constants/Data/AppConfiguration.json b/src/dotnet/Common/Constants/Data/AppConfiguration.json index d1248db09..b75dc720b 100644 --- a/src/dotnet/Common/Constants/Data/AppConfiguration.json +++ b/src/dotnet/Common/Constants/Data/AppConfiguration.json @@ -80,6 +80,14 @@ "content_type": "", "first_version": "0.9.0" }, + { + "name": "Logging:LogLevel:Azure", + "description": "The default logging level used by the Python SDK.", + "secret": "", + "value": "Warning", + "content_type": "", + "first_version": "0.9.1" + }, { "name": "Logging:EnableConsoleLogging", "description": "The flag indicating whether the Python SDK sends logs to the console or not.", diff --git a/src/dotnet/Common/Models/ResourceProviders/Agent/AgentWorkflows/AgentWorkflowBase.cs b/src/dotnet/Common/Models/ResourceProviders/Agent/AgentWorkflows/AgentWorkflowBase.cs index 5d06455c9..db5756271 100644 --- a/src/dotnet/Common/Models/ResourceProviders/Agent/AgentWorkflows/AgentWorkflowBase.cs +++ b/src/dotnet/Common/Models/ResourceProviders/Agent/AgentWorkflows/AgentWorkflowBase.cs @@ -10,6 +10,7 @@ namespace FoundationaLLM.Common.Models.ResourceProviders.Agent.AgentWorkflows [JsonDerivedType(typeof(AzureOpenAIAssistantsAgentWorkflow), AgentWorkflowTypes.AzureOpenAIAssistants)] [JsonDerivedType(typeof(LangChainExpressionLanguageAgentWorkflow), AgentWorkflowTypes.LangChainExpressionLanguage)] [JsonDerivedType(typeof(LangGraphReactAgentWorkflow), AgentWorkflowTypes.LangGraphReactAgent)] + [JsonDerivedType(typeof(ExternalAgentWorkflow), AgentWorkflowTypes.ExternalAgentWorkflow)] public class AgentWorkflowBase { /// @@ -20,12 +21,17 @@ public class AgentWorkflowBase /// /// The name of the workflow. + /// + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// + /// Gets or sets the package name of the workflow. + /// For internal workflows, this value will be FoundationaLLM + /// For external workflows, this value will be the name of the package. /// - /// - /// This value is always derived from the property. - /// - [JsonPropertyName("workflow_name")] - public string? WorkflowName { get; set; } + [JsonPropertyName("package_name")] + public required string PackageName { get; set; } /// /// The host of the workflow environment. diff --git a/src/dotnet/Common/Models/ResourceProviders/Agent/AgentWorkflows/AgentWorkflowTypes.cs b/src/dotnet/Common/Models/ResourceProviders/Agent/AgentWorkflows/AgentWorkflowTypes.cs index aacae1b56..92af86a4d 100644 --- a/src/dotnet/Common/Models/ResourceProviders/Agent/AgentWorkflows/AgentWorkflowTypes.cs +++ b/src/dotnet/Common/Models/ResourceProviders/Agent/AgentWorkflows/AgentWorkflowTypes.cs @@ -19,5 +19,10 @@ public static class AgentWorkflowTypes /// The LangGraph ReAct agent workflow. /// public const string LangGraphReactAgent = "langgraph-react-agent-workflow"; + + /// + /// The External Agent workflow. + /// + public const string ExternalAgentWorkflow = "external-agent-workflow"; } } diff --git a/src/dotnet/Common/Models/ResourceProviders/Agent/AgentWorkflows/ExternalAgentWorkflow.cs b/src/dotnet/Common/Models/ResourceProviders/Agent/AgentWorkflows/ExternalAgentWorkflow.cs new file mode 100644 index 000000000..0b7ed5ae1 --- /dev/null +++ b/src/dotnet/Common/Models/ResourceProviders/Agent/AgentWorkflows/ExternalAgentWorkflow.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace FoundationaLLM.Common.Models.ResourceProviders.Agent.AgentWorkflows +{ + /// + /// Provides an agent workflow configuration for an External Agent workflow loaded via an external module. + /// + public class ExternalAgentWorkflow : AgentWorkflowBase + { + /// + [JsonIgnore] + public override string Type => AgentWorkflowTypes.ExternalAgentWorkflow; + } +} diff --git a/src/dotnet/Common/Templates/AppConfigurationKeys.cs b/src/dotnet/Common/Templates/AppConfigurationKeys.cs index efeccf41a..3dbd555fe 100644 --- a/src/dotnet/Common/Templates/AppConfigurationKeys.cs +++ b/src/dotnet/Common/Templates/AppConfigurationKeys.cs @@ -71,6 +71,13 @@ public static class AppConfigurationKeys public const string FoundationaLLM_PythonSDK_Logging_LogLevel_Default = "FoundationaLLM:PythonSDK:Logging:LogLevel:Default"; + /// + /// The app configuration key for the FoundationaLLM:PythonSDK:Logging:LogLevel:Azure setting. + /// Value description:
The default logging level used by the Python SDK.
+ ///
+ public const string FoundationaLLM_PythonSDK_Logging_LogLevel_Azure = + "FoundationaLLM:PythonSDK:Logging:LogLevel:Azure"; + /// /// The app configuration key for the FoundationaLLM:PythonSDK:Logging:EnableConsoleLogging setting. /// Value description:
The flag indicating whether the Python SDK sends logs to the console or not.
diff --git a/src/dotnet/Common/Templates/appconfig.template.json b/src/dotnet/Common/Templates/appconfig.template.json index 30821aaf7..89b4c0a5a 100644 --- a/src/dotnet/Common/Templates/appconfig.template.json +++ b/src/dotnet/Common/Templates/appconfig.template.json @@ -56,6 +56,13 @@ "content_type": "", "tags": {} }, + { + "key": "FoundationaLLM:PythonSDK:Logging:LogLevel:Azure", + "value": "Warning", + "label": null, + "content_type": "", + "tags": {} + }, { "key": "FoundationaLLM:PythonSDK:Logging:EnableConsoleLogging", "value": "false", diff --git a/src/dotnet/Orchestration/Orchestration/OrchestrationBuilder.cs b/src/dotnet/Orchestration/Orchestration/OrchestrationBuilder.cs index 62ba02c7d..8dc5b1285 100644 --- a/src/dotnet/Orchestration/Orchestration/OrchestrationBuilder.cs +++ b/src/dotnet/Orchestration/Orchestration/OrchestrationBuilder.cs @@ -285,7 +285,15 @@ await cosmosDBService.PatchOperationsItemPropertiesAsync( + resourceObjectId.ObjectId, + currentUserIdentity); + + explodedObjectsManager.TryAdd( + retrievedWorkflow.ObjectId!, + retrievedWorkflow); + break; } } diff --git a/src/python/Experimental/src/skunkworks_foundationallm/__init__.py b/src/python/Experimental/src/skunkworks_foundationallm/__init__.py index 78e91f5c9..9d2c33569 100644 --- a/src/python/Experimental/src/skunkworks_foundationallm/__init__.py +++ b/src/python/Experimental/src/skunkworks_foundationallm/__init__.py @@ -1 +1,2 @@ from .skunkworks_tool_plugin_manager import SkunkworksToolPluginManager +from .skunkworks_workflow_plugin_manager import SkunkworksWorkflowPluginManager \ No newline at end of file diff --git a/src/python/Experimental/src/skunkworks_foundationallm/skunkworks_tool_plugin_manager.py b/src/python/Experimental/src/skunkworks_foundationallm/skunkworks_tool_plugin_manager.py index 888ce180b..c83bebf60 100644 --- a/src/python/Experimental/src/skunkworks_foundationallm/skunkworks_tool_plugin_manager.py +++ b/src/python/Experimental/src/skunkworks_foundationallm/skunkworks_tool_plugin_manager.py @@ -14,7 +14,7 @@ class SkunkworksToolPluginManager(ToolPluginManagerBase): FOUNDATIONALLM_CODE_INTERPRETER_TOOL_NAME = 'FoundationaLLMCodeInterpreterTool' def __init__(self): - pass + super().__init__() def create_tool(self, tool_config: AgentTool, diff --git a/src/python/Experimental/src/skunkworks_foundationallm/skunkworks_workflow_plugin_manager.py b/src/python/Experimental/src/skunkworks_foundationallm/skunkworks_workflow_plugin_manager.py new file mode 100644 index 000000000..2106ad3a2 --- /dev/null +++ b/src/python/Experimental/src/skunkworks_foundationallm/skunkworks_workflow_plugin_manager.py @@ -0,0 +1,48 @@ +from typing import List + +from foundationallm.config import Configuration, UserIdentity +from foundationallm.models.agents import AgentTool, ExternalAgentWorkflow +from foundationallm.langchain.common import FoundationaLLMWorkflowBase +from foundationallm.plugins import WorkflowPluginManagerBase + +from skunkworks_foundationallm.workflows import ( + FoundationaLLMRouterWorkflow +) + +class SkunkworksWorkflowPluginManager(WorkflowPluginManagerBase): + + FOUNDATIONALLM_ROUTER_WORKFLOW_NAME = 'FoundationaLLMRouterWorkflow' + + def __init__(self): + super().__init__() + + def create_workflow( + self, + workflow_config: ExternalAgentWorkflow, + objects: dict, + tools: List[AgentTool], + user_identity: UserIdentity, + config: Configuration) -> FoundationaLLMWorkflowBase: + """ + Create a workflow instance based on the given configuration and tools. + Parameters + ---------- + workflow_config : ExternalAgentWorkflow + The workflow assigned to the agent. + objects : dict + The exploded objects assigned from the agent. + tools : List[AgentTool] + The tools assigned to the agent. + user_identity : UserIdentity + The user identity of the user initiating the request. + config : Configuration + The application configuration for FoundationaLLM. + """ + match workflow_config.name: + case SkunkworksWorkflowPluginManager.FOUNDATIONALLM_ROUTER_WORKFLOW_NAME: + return FoundationaLLMRouterWorkflow(workflow_config, objects, tools, user_identity, config) + case _: + raise ValueError(f"Unknown tool name: {workflow_config.name}") + + def refresh_tools(): + print('Refreshing tools...') diff --git a/src/python/Experimental/src/skunkworks_foundationallm/workflows/__init__.py b/src/python/Experimental/src/skunkworks_foundationallm/workflows/__init__.py new file mode 100644 index 000000000..7fc46b819 --- /dev/null +++ b/src/python/Experimental/src/skunkworks_foundationallm/workflows/__init__.py @@ -0,0 +1 @@ +from .foundationallm_router_workflow import FoundationaLLMRouterWorkflow \ No newline at end of file diff --git a/src/python/Experimental/src/skunkworks_foundationallm/workflows/foundationallm_router_workflow.py b/src/python/Experimental/src/skunkworks_foundationallm/workflows/foundationallm_router_workflow.py new file mode 100644 index 000000000..0d29d9ad3 --- /dev/null +++ b/src/python/Experimental/src/skunkworks_foundationallm/workflows/foundationallm_router_workflow.py @@ -0,0 +1,77 @@ +""" +Class: FoundationaLLMWorkflowBase +Description: FoundationaLLM base class for tools that uses the agent workflow model for its configuration. +""" +from abc import ABC, abstractmethod +from azure.identity import DefaultAzureCredential +from langchain_core.messages import BaseMessage +from pydantic import BaseModel +from typing import List +from foundationallm.config import Configuration, UserIdentity +from foundationallm.langchain.common import FoundationaLLMWorkflowBase +from foundationallm.models.agents import AgentTool, ExternalAgentWorkflow +from foundationallm.models.constants import AgentCapabilityCategories +from foundationallm.models.orchestration import CompletionResponse, OpenAITextMessageContentItem +from foundationallm.telemetry import Telemetry + +class FoundationaLLMRouterWorkflow(FoundationaLLMWorkflowBase): + """ + FoundationaLLM base class for workflows that uses the agent workflow model for its configuration. + """ + def __init__(self, + workflow_config: ExternalAgentWorkflow, + objects: dict, + tools: List[AgentTool], + user_identity: UserIdentity, + config: Configuration): + """ + Initializes the FoundationaLLMWorkflowBase class with the workflow configuration. + + Parameters + ---------- + workflow_config : ExternalAgentWorkflow + The workflow assigned to the agent. + objects : dict + The exploded objects assigned from the agent. + tools : List[AgentTool] + The tools assigned to the agent. + user_identity : UserIdentity + The user identity of the user initiating the request. + config : Configuration + The application configuration for FoundationaLLM. + """ + super().__init__(workflow_config, objects, tools, user_identity, config) + + async def invoke_async(self, + operation_id: str, + user_prompt:str, + message_history: List[BaseMessage])-> CompletionResponse: + """ + Invokes the workflow asynchronously. + + Parameters + ---------- + operation_id : str + The unique identifier of the FoundationaLLM operation. + user_prompt : str + The user prompt message. + message_history : List[BaseMessage] + The message history. + """ + response_content = OpenAITextMessageContentItem( + value = '42 is the answer to all questions', + agent_capability_category = AgentCapabilityCategories.FOUNDATIONALLM_KNOWLEDGE_MANAGEMENT + ) + + + return CompletionResponse( + operation_id = operation_id, + content = [response_content], + content_artifacts = [], + user_prompt = user_prompt, + full_prompt = '', + completion_tokens = 0, + prompt_tokens = 0, + total_tokens = 0, + total_cost = 0 + ) diff --git a/src/python/PythonSDK/PythonSDK.pyproj b/src/python/PythonSDK/PythonSDK.pyproj index ab67ad03f..b69214c07 100644 --- a/src/python/PythonSDK/PythonSDK.pyproj +++ b/src/python/PythonSDK/PythonSDK.pyproj @@ -30,11 +30,15 @@ + + + + @@ -56,7 +60,9 @@ + + @@ -185,12 +191,14 @@ + + diff --git a/src/python/PythonSDK/foundationallm/langchain/agents/langchain_knowledge_management_agent.py b/src/python/PythonSDK/foundationallm/langchain/agents/langchain_knowledge_management_agent.py index e19faacec..5e70c574a 100644 --- a/src/python/PythonSDK/foundationallm/langchain/agents/langchain_knowledge_management_agent.py +++ b/src/python/PythonSDK/foundationallm/langchain/agents/langchain_knowledge_management_agent.py @@ -8,7 +8,8 @@ from foundationallm.langchain.agents import LangChainAgentBase from foundationallm.langchain.exceptions import LangChainException from foundationallm.langchain.retrievers import RetrieverFactory, ContentArtifactRetrievalBase -from foundationallm.models.agents import AzureOpenAIAssistantsAgentWorkflow, LangGraphReactAgentWorkflow +from foundationallm.langchain.workflows import WorkflowFactory +from foundationallm.models.agents import AzureOpenAIAssistantsAgentWorkflow, ExternalAgentWorkflow, LangGraphReactAgentWorkflow from foundationallm.models.constants import ( AgentCapabilityCategories, ResourceObjectIdPropertyNames, @@ -499,6 +500,43 @@ async def invoke_async(self, request: KnowledgeManagementCompletionRequest) -> C ) # End LangGraph ReAct Agent workflow implementation + # Start External Agent workflow implementation + if (agent.workflow is not None and isinstance(agent.workflow, ExternalAgentWorkflow)): + # prepare tools + tool_factory = ToolFactory(self.plugin_manager) + tools = [] + + parsed_user_prompt = request.user_prompt + + explicit_tool = next((tool for tool in agent.tools if parsed_user_prompt.startswith(f'[{tool.name}]:')), None) + if explicit_tool is not None: + tools.append(tool_factory.get_tool(explicit_tool, request.objects, self.user_identity, self.config)) + parsed_user_prompt = parsed_user_prompt.split(':', 1)[1].strip() + else: + # Populate tools list from agent configuration + for tool in agent.tools: + tools.append(tool_factory.get_tool(tool, request.objects, self.user_identity, self.config)) + + # create the workflow + workflow_factory = WorkflowFactory(self.plugin_manager) + workflow = workflow_factory.get_workflow( + agent.workflow, + request.objects, + tools, + self.user_identity, + self.config) + + # Get message history + messages = self._build_conversation_history_message_list(request.message_history, agent.conversation_history_settings.max_history) + + response = await workflow.invoke_async( + operation_id=request.operation_id, + user_prompt=parsed_user_prompt, + message_history=messages + ) + return response + # End External Agent workflow implementation + # Start LangChain Expression Language (LCEL) implementation # Get the vector document retriever, if it exists. diff --git a/src/python/PythonSDK/foundationallm/langchain/common/__init__.py b/src/python/PythonSDK/foundationallm/langchain/common/__init__.py index 108e083d0..81fe4eb03 100644 --- a/src/python/PythonSDK/foundationallm/langchain/common/__init__.py +++ b/src/python/PythonSDK/foundationallm/langchain/common/__init__.py @@ -1 +1,2 @@ from .foundationallm_tool_base import FoundationaLLMToolBase +from .foundationallm_workflow_base import FoundationaLLMWorkflowBase diff --git a/src/python/PythonSDK/foundationallm/langchain/common/foundationallm_workflow_base.py b/src/python/PythonSDK/foundationallm/langchain/common/foundationallm_workflow_base.py new file mode 100644 index 000000000..c16fe40cc --- /dev/null +++ b/src/python/PythonSDK/foundationallm/langchain/common/foundationallm_workflow_base.py @@ -0,0 +1,66 @@ +""" +Class: FoundationaLLMWorkflowBase +Description: FoundationaLLM base class for tools that uses the agent workflow model for its configuration. +""" +from abc import ABC, abstractmethod +from azure.identity import DefaultAzureCredential +from langchain_core.messages import BaseMessage +from typing import List +from foundationallm.config import Configuration, UserIdentity +from foundationallm.models.agents import AgentTool, ExternalAgentWorkflow +from foundationallm.models.orchestration import CompletionResponse +from foundationallm.telemetry import Telemetry + +class FoundationaLLMWorkflowBase(ABC): + """ + FoundationaLLM base class for workflows that uses the agent workflow model for its configuration. + """ + def __init__(self, + workflow_config: ExternalAgentWorkflow, + objects: dict, + tools: List[AgentTool], + user_identity: UserIdentity, + config: Configuration): + """ + Initializes the FoundationaLLMWorkflowBase class with the workflow configuration. + + Parameters + ---------- + workflow_config : ExternalAgentWorkflow + The workflow assigned to the agent. + objects : dict + The exploded objects assigned from the agent. + tools : List[AgentTool] + The tools assigned to the agent. + user_identity : UserIdentity + The user identity of the user initiating the request. + config : Configuration + The application configuration for FoundationaLLM. + """ + self.workflow_config = workflow_config + self.objects = objects + self.tools = tools if tools is not None else [] + self.user_identity = user_identity + self.config = config + self.logger = Telemetry.get_logger(self.workflow_config.name) + self.tracer = Telemetry.get_tracer(self.workflow_config.name) + self.default_credential = DefaultAzureCredential(exclude_environment_credential=True) + + @abstractmethod + async def invoke_async(self, + operation_id: str, + user_prompt:str, + message_history: List[BaseMessage])-> CompletionResponse: + """ + Invokes the workflow asynchronously. + + Parameters + ---------- + operation_id : str + The unique identifier of the FoundationaLLM operation. + user_prompt : str + The user prompt message. + message_history : List[BaseMessage] + The message history. + """ + pass diff --git a/src/python/PythonSDK/foundationallm/langchain/tools/tool_factory.py b/src/python/PythonSDK/foundationallm/langchain/tools/tool_factory.py index 05f072387..207636af0 100644 --- a/src/python/PythonSDK/foundationallm/langchain/tools/tool_factory.py +++ b/src/python/PythonSDK/foundationallm/langchain/tools/tool_factory.py @@ -7,7 +7,7 @@ from foundationallm.langchain.exceptions import LangChainException from foundationallm.langchain.tools import DALLEImageGenerationTool from foundationallm.models.agents import AgentTool -from foundationallm.plugins import PluginManager +from foundationallm.plugins import PluginManager, PluginManagerTypes class ToolFactory: """ @@ -46,7 +46,12 @@ def get_tool( tool_plugin_manager = None if tool_config.package_name in self.plugin_manager.external_modules: - tool_plugin_manager = self.plugin_manager.external_modules[tool_config.package_name].tool_plugin_manager + tool_plugin_manager = next(( \ + pm for pm \ + in self.plugin_manager.external_modules[tool_config.package_name].plugin_managers \ + if pm.plugin_manager_type == PluginManagerTypes.TOOLS), None) + if tool_plugin_manager is None: + raise LangChainException(f"Tool plugin manager not found for package {tool_config.package_name}") return tool_plugin_manager.create_tool(tool_config, objects, user_identity, config) else: raise LangChainException(f"Package {tool_config.package_name} not found in the list of external modules loaded by the package manager.") diff --git a/src/python/PythonSDK/foundationallm/langchain/workflows/__init__.py b/src/python/PythonSDK/foundationallm/langchain/workflows/__init__.py new file mode 100644 index 000000000..b17874243 --- /dev/null +++ b/src/python/PythonSDK/foundationallm/langchain/workflows/__init__.py @@ -0,0 +1 @@ +from .workflow_factory import WorkflowFactory diff --git a/src/python/PythonSDK/foundationallm/langchain/workflows/workflow_factory.py b/src/python/PythonSDK/foundationallm/langchain/workflows/workflow_factory.py new file mode 100644 index 000000000..ed0dae954 --- /dev/null +++ b/src/python/PythonSDK/foundationallm/langchain/workflows/workflow_factory.py @@ -0,0 +1,68 @@ +""" +Class: WorkflowFactory +Description: Factory class for creating an external workflow instance based on the Agent workflow configuration. +""" +from typing import List +from foundationallm.config import Configuration, UserIdentity +from foundationallm.langchain.common import FoundationaLLMWorkflowBase +from foundationallm.langchain.exceptions import LangChainException +from foundationallm.models.agents import AgentTool, ExternalAgentWorkflow +from foundationallm.plugins import PluginManager, PluginManagerTypes + +class WorkflowFactory: + """ + Factory class for creating an external agent workflow instance based on the Agent workflow configuration. + """ + def __init__(self, plugin_manager: PluginManager): + """ + Initializes the workflow factory. + + Parameters + ---------- + plugin_manager : PluginManager + The plugin manager object used to load external workflows. + """ + self.plugin_manager = plugin_manager + + def get_workflow( + self, + workflow_config: ExternalAgentWorkflow, + objects: dict, + tools: List[AgentTool], + user_identity: UserIdentity, + config: Configuration + ) -> FoundationaLLMWorkflowBase: + """ + Creates an instance of an agent workflow based on the agent workflow configuration. + + Parameters + ---------- + workflow_config : ExternalAgentWorkflow + The workflow assigned to the agent. + objects : dict + The exploded objects assigned from the agent. + tools : List[AgentTool] + The tools assigned to the agent. + user_identity : UserIdentity + The user identity of the user initiating the request. + config : Configuration + The application configuration for FoundationaLLM. + """ + + if workflow_config.package_name == "FoundationaLLM": + # internal workflows + # TODO: Refactor internal workflows to use the plugin manager + raise LangChainException("Internal workflows are not supported by the plugin manager.") + else: + workflow_plugin_manager = None + + if workflow_config.package_name in self.plugin_manager.external_modules: + workflow_plugin_manager = next(( \ + wm for wm \ + in self.plugin_manager.external_modules[workflow_config.package_name].plugin_managers \ + if wm.plugin_manager_type == PluginManagerTypes.WORKFLOWS), None) + if workflow_plugin_manager is None: + raise LangChainException(f"Workflow plugin manager not found for package {workflow_config.package_name}") + return workflow_plugin_manager.create_workflow(workflow_config, objects, tools, user_identity, config) + else: + raise LangChainException(f"Package {workflow_config.package_name} not found in the list of external modules loaded by the package manager.") diff --git a/src/python/PythonSDK/foundationallm/models/agents/__init__.py b/src/python/PythonSDK/foundationallm/models/agents/__init__.py index 1e6755bf4..0f426cfe2 100644 --- a/src/python/PythonSDK/foundationallm/models/agents/__init__.py +++ b/src/python/PythonSDK/foundationallm/models/agents/__init__.py @@ -4,6 +4,7 @@ from .agent_tool import AgentTool from .agent_vectorization_settings import AgentVectorizationSettings from .agent_workflows.agent_workflow_ai_model import AgentWorkflowAIModel +from .agent_workflows.external_agent_workflow import ExternalAgentWorkflow from .agent_workflows.agent_workflow_base import AgentWorkflowBase from .agent_workflows.azure_openai_assistants_agent_workflow import AzureOpenAIAssistantsAgentWorkflow from .agent_workflows.langchain_expression_language_agent_workflow import LangChainExpressionLanguageAgentWorkflow diff --git a/src/python/PythonSDK/foundationallm/models/agents/agent_base.py b/src/python/PythonSDK/foundationallm/models/agents/agent_base.py index b7aeafbbd..e2a4a193c 100644 --- a/src/python/PythonSDK/foundationallm/models/agents/agent_base.py +++ b/src/python/PythonSDK/foundationallm/models/agents/agent_base.py @@ -10,6 +10,7 @@ AzureOpenAIAssistantsAgentWorkflow, LangChainExpressionLanguageAgentWorkflow, LangGraphReactAgentWorkflow, + ExternalAgentWorkflow, AgentTool ) from foundationallm.models.resource_providers import ResourceBase @@ -26,7 +27,7 @@ class AgentBase(ResourceBase): tools: Optional[List[AgentTool]] = Field(default=[], description="A list of assigned agent tools.") workflow: Optional[ Annotated [ - Union[AzureOpenAIAssistantsAgentWorkflow, LangChainExpressionLanguageAgentWorkflow, LangGraphReactAgentWorkflow], + Union[AzureOpenAIAssistantsAgentWorkflow, LangChainExpressionLanguageAgentWorkflow, LangGraphReactAgentWorkflow, ExternalAgentWorkflow], Field(discriminator='type') ] ]= Field(default=None, description="The workflow configuration for the agent.") diff --git a/src/python/PythonSDK/foundationallm/models/agents/agent_workflows/agent_workflow_base.py b/src/python/PythonSDK/foundationallm/models/agents/agent_workflows/agent_workflow_base.py index 7e6e80da7..f693e05d5 100644 --- a/src/python/PythonSDK/foundationallm/models/agents/agent_workflows/agent_workflow_base.py +++ b/src/python/PythonSDK/foundationallm/models/agents/agent_workflows/agent_workflow_base.py @@ -10,7 +10,8 @@ class AgentWorkflowBase(ResourceObjectIdsModelBase): """ type: Optional[str] = Field(None, alias="type") workflow_host: str = Field(None, alias="workflow_host") - workflow_name: str = Field(None, alias="workflow_name") + name: str = Field(None, alias="name") + package_name: str=Field(None, alias="package_name") @staticmethod def from_object(obj: Any) -> Self: diff --git a/src/python/PythonSDK/foundationallm/models/agents/agent_workflows/external_agent_workflow.py b/src/python/PythonSDK/foundationallm/models/agents/agent_workflows/external_agent_workflow.py new file mode 100644 index 000000000..7eb7c3cc7 --- /dev/null +++ b/src/python/PythonSDK/foundationallm/models/agents/agent_workflows/external_agent_workflow.py @@ -0,0 +1,26 @@ +from typing import Any, Self, Literal +from foundationallm.langchain.exceptions import LangChainException +from foundationallm.utils import object_utils +from .agent_workflow_base import AgentWorkflowBase + +class ExternalAgentWorkflow(AgentWorkflowBase): + """ + The configuration for an external agent workflow loaded as a plugin. + """ + type: Literal["external-agent-workflow"] = "external-agent-workflow" + + + @staticmethod + def from_object(obj: Any) -> Self: + + workflow: ExternalAgentWorkflow = None + + try: + workflow = ExternalAgentWorkflow(**object_utils.translate_keys(obj)) + except Exception as e: + raise LangChainException(f"The External Agent Workflow object provided is invalid. {str(e)}", 400) + + if workflow is None: + raise LangChainException("The External Agent Workflow object provided is invalid.", 400) + + return workflow diff --git a/src/python/PythonSDK/foundationallm/plugins/__init__.py b/src/python/PythonSDK/foundationallm/plugins/__init__.py index 1b428b0f5..9571d4a9a 100644 --- a/src/python/PythonSDK/foundationallm/plugins/__init__.py +++ b/src/python/PythonSDK/foundationallm/plugins/__init__.py @@ -1,2 +1,4 @@ +from .plugin_manager_types import PluginManagerTypes from .plugin_manager import PluginManager from .tools.tool_plugin_manager_base import ToolPluginManagerBase +from .workflows.workflow_plugin_manager_base import WorkflowPluginManagerBase diff --git a/src/python/PythonSDK/foundationallm/plugins/external_module.py b/src/python/PythonSDK/foundationallm/plugins/external_module.py index 417e6c09e..5714fb21b 100644 --- a/src/python/PythonSDK/foundationallm/plugins/external_module.py +++ b/src/python/PythonSDK/foundationallm/plugins/external_module.py @@ -1,5 +1,6 @@ from types import ModuleType - +from typing import Union, List +from .workflows.workflow_plugin_manager_base import WorkflowPluginManagerBase from .tools.tool_plugin_manager_base import ToolPluginManagerBase class ExternalModule(): @@ -9,18 +10,18 @@ class ExternalModule(): module_name: str - The name of the module. module_loaded: bool - Indicates whether the module is loaded. module: ModuleType - The module object. - tool_plugin_manager_class_name: str - The name of the tool plugin manager class for the module. - tool_plugin_manager: ToolPluginManager - The tool plugin manager for the module. + plugin_manager_class_names: List[str] - The list of plugin manager class names for the module. + plugin_manager: List[Union[ToolPluginManager, WorkflowPluginManager]] - The list of plugin managers for the module. """ module_file: str module_name: str module_loaded: bool = False module: ModuleType = None - tool_plugin_manager_class_name: str = None - tool_plugin_manager: ToolPluginManagerBase = None + plugin_manager_class_names: List[str] = None + plugin_managers: List[Union[ToolPluginManagerBase, WorkflowPluginManagerBase]] = None - def __init__(self, module_file: str, module_name: str, tool_plugin_manager_class_name: str): + def __init__(self, module_file: str, module_name: str, plugin_manager_class_names: List[str]): """ Initializes the external module. @@ -30,9 +31,10 @@ def __init__(self, module_file: str, module_name: str, tool_plugin_manager_class The name of the module file. module_name : str The name of the module. - tool_plugin_manager_class_name : str - The name of the tool plugin manager class for the module. + plugin_manager_class_name : List[Union[ToolPluginManager, WorkflowPluginManager]] + The list of plugin managers for the module. """ self.module_file = module_file self.module_name = module_name - self.tool_plugin_manager_class_name = tool_plugin_manager_class_name + self.plugin_manager_class_names = plugin_manager_class_names + self.plugin_managers = [] diff --git a/src/python/PythonSDK/foundationallm/plugins/plugin_manager.py b/src/python/PythonSDK/foundationallm/plugins/plugin_manager.py index c40773e31..ae55a8ee0 100644 --- a/src/python/PythonSDK/foundationallm/plugins/plugin_manager.py +++ b/src/python/PythonSDK/foundationallm/plugins/plugin_manager.py @@ -4,6 +4,7 @@ import sys from .external_module import ExternalModule +from .plugin_manager_types import PluginManagerTypes from foundationallm.config import Configuration from foundationallm.storage import BlobStorageManager @@ -15,9 +16,6 @@ PLUGIN_MANAGER_MODULES = f'{PLUGIN_MANAGER_CONFIGURATION_NAMESPACE}:Modules' PLUGIN_MANAGER_LOCAL_STORAGE_FOLDER_NAME = 'foundationallm_external_modules' -TOOLS_PLUGIN_MANAGER_TYPE = 'tools' - - class PluginManager(): """ Manages the plugins in the system. @@ -80,18 +78,17 @@ def __init__(self, config:Configuration, logger:Logger): plugin_manager_type = module_configuration[2] plugin_manager_class_name = module_configuration[3] - if (plugin_manager_type != TOOLS_PLUGIN_MANAGER_TYPE): + if (plugin_manager_type != PluginManagerTypes.TOOLS and plugin_manager_type != PluginManagerTypes.WORKFLOWS): raise ValueError(f'The plugin manager type {plugin_manager_type} is not recognized.') if module_name in self.external_modules: - self.external_modules[module_name].tool_plugin_manager_class_name = plugin_manager_class_name + self.external_modules[module_name].plugin_manager_class_names.append(plugin_manager_class_name) else: self.external_modules[module_name] = ExternalModule( module_file=module_file, module_name=module_name, - tool_plugin_manager_class_name=plugin_manager_class_name - ) - + plugin_manager_class_names=[plugin_manager_class_name] + ) self.initialized = True self.logger.info('The plugin manager initialized successfully.') @@ -105,6 +102,8 @@ def load_external_modules(self): if not self.initialized: self.logger.error('The plugin manager is not initialized. No plugins will be loaded.') return + + loaded_modules = set() for module_name in self.external_modules.keys(): @@ -114,20 +113,27 @@ def load_external_modules(self): self.logger.info(f'Loading module from {module_file_name}') try: - if (self.storage_manager.file_exists(module_file_name)): - self.logger.info(f'Copying module file to: {local_module_file_name}') - module_file_binary_content = self.storage_manager.read_file_content(module_file_name) - with open(local_module_file_name, 'wb') as f: - f.write(module_file_binary_content) - - sys.path.insert(0, local_module_file_name) - external_module.module = import_module(external_module.module_name) - - self.logger.info(f'Module {module_name} loaded successfully.') - external_module.module_loaded = True - # Note the () at the end of the getattr call - this is to call the class constructor, not just get the class. - external_module.tool_plugin_manager = getattr(external_module.module, external_module.tool_plugin_manager_class_name)() + if module_name in loaded_modules: + self.logger.info(f'Module {module_name} and all plugin managers are already loaded.') + continue + else: + if (self.storage_manager.file_exists(module_file_name)): + self.logger.info(f'Copying module file to: {local_module_file_name}') + module_file_binary_content = self.storage_manager.read_file_content(module_file_name) + with open(local_module_file_name, 'wb') as f: + f.write(module_file_binary_content) + + sys.path.insert(0, local_module_file_name) + external_module.module = import_module(external_module.module_name) + + self.logger.info(f'Module {module_name} loaded successfully.') + external_module.module_loaded = True + loaded_modules.add(module_name) + + for plugin_manager_class_name in external_module.plugin_manager_class_names: + # Note the () at the end of the getattr call - this is to call the class constructor, not just get the class. + external_module.plugin_managers.append(getattr(external_module.module, plugin_manager_class_name)()) except Exception as e: self.logger.exception(f'An error occurred while loading module: {module_name}') diff --git a/src/python/PythonSDK/foundationallm/plugins/plugin_manager_types.py b/src/python/PythonSDK/foundationallm/plugins/plugin_manager_types.py new file mode 100644 index 000000000..781a49a72 --- /dev/null +++ b/src/python/PythonSDK/foundationallm/plugins/plugin_manager_types.py @@ -0,0 +1,6 @@ +from enum import Enum + +class PluginManagerTypes(str, Enum): + """Enumerator of the Plugin Manager Types.""" + TOOLS = "tools" + WORKFLOWS = "workflows" diff --git a/src/python/PythonSDK/foundationallm/plugins/tools/tool_plugin_manager_base.py b/src/python/PythonSDK/foundationallm/plugins/tools/tool_plugin_manager_base.py index bea238d75..6c1dab64b 100644 --- a/src/python/PythonSDK/foundationallm/plugins/tools/tool_plugin_manager_base.py +++ b/src/python/PythonSDK/foundationallm/plugins/tools/tool_plugin_manager_base.py @@ -4,10 +4,12 @@ from foundationallm.config import Configuration, UserIdentity from foundationallm.langchain.common import FoundationaLLMToolBase from foundationallm.models.agents import AgentTool +from foundationallm.plugins import PluginManagerTypes class ToolPluginManagerBase(ABC): def __init__(self): - pass + + self.plugin_manager_type = PluginManagerTypes.TOOLS @abstractmethod def create_tool(self, diff --git a/src/python/PythonSDK/foundationallm/plugins/workflows/workflow_plugin_manager_base.py b/src/python/PythonSDK/foundationallm/plugins/workflows/workflow_plugin_manager_base.py new file mode 100644 index 000000000..edb8a8643 --- /dev/null +++ b/src/python/PythonSDK/foundationallm/plugins/workflows/workflow_plugin_manager_base.py @@ -0,0 +1,42 @@ +from abc import ABC, abstractmethod +from typing import List +from foundationallm.config import Configuration, UserIdentity +from foundationallm.langchain.common import FoundationaLLMWorkflowBase +from foundationallm.models.agents import AgentTool, ExternalAgentWorkflow +from foundationallm.plugins import PluginManagerTypes + +class WorkflowPluginManagerBase(ABC): + """ + The base class for all workflow plugin managers. + """ + def __init__(self): + + self.plugin_manager_type = PluginManagerTypes.WORKFLOWS + + @abstractmethod + def create_workflow(self, + workflow_config: ExternalAgentWorkflow, + objects: dict, + tools: List[AgentTool], + user_identity: UserIdentity, + config: Configuration) -> FoundationaLLMWorkflowBase: + """ + Create a workflow instance based on the given configuration and tools. + Parameters + ---------- + workflow_config : ExternalAgentWorkflow + The workflow assigned to the agent. + objects : dict + The exploded objects assigned from the agent. + tools : List[AgentTool] + The tools assigned to the agent. + user_identity : UserIdentity + The user identity of the user initiating the request. + config : Configuration + The application configuration for FoundationaLLM. + """ + pass + + @abstractmethod + def refresh_tools(): + pass diff --git a/src/python/PythonSDK/foundationallm/telemetry/telemetry.py b/src/python/PythonSDK/foundationallm/telemetry/telemetry.py index d64c0dd18..930b5cde0 100644 --- a/src/python/PythonSDK/foundationallm/telemetry/telemetry.py +++ b/src/python/PythonSDK/foundationallm/telemetry/telemetry.py @@ -24,6 +24,7 @@ class Telemetry: """ log_level : int = logging.WARNING + azure_log_level : int = logging.WARNING langchain_log_level : int = logging.NOTSET api_name : str = None telemetry_connection_string : str = None @@ -117,41 +118,58 @@ def record_exception(span: Span, ex: Exception): span.set_status(Status(StatusCode.ERROR)) span.record_exception(ex) + @staticmethod + def translate_log_level(log_level: str) -> int: + """ + Translates a log level string to a logging level. + + Parameters + ---------- + log_level : str + The log level to translate. + + Returns + ------- + int + Returns the logging level for the specified log level string. + """ + if log_level == "Debug": + return logging.DEBUG + elif log_level == "Trace": + return logging.DEBUG + elif log_level == "Information": + return logging.INFO + elif log_level == "Warning": + return logging.WARNING + elif log_level == "Error": + return logging.ERROR + elif log_level == "Critical": + return logging.CRITICAL + else: + return logging.NOTSET + @staticmethod def configure_logging(config: Configuration): - #Get dotnet log level - str_log_level = config.get_value("FoundationaLLM:PythonSDK:Logging:LogLevel:Default") + + Telemetry.log_level = Telemetry.translate_log_level( + config.get_value("FoundationaLLM:PythonSDK:Logging:LogLevel:Default")) + + Telemetry.azure_log_level = Telemetry.translate_log_level( + config.get_value("FoundationaLLM:PythonSDK:Logging:LogLevel:Azure")) enable_console_logging = config.get_value("FoundationaLLM:PythonSDK:Logging:EnableConsoleLogging") #Log output handlers handlers = [] + set_debug(False) + set_verbose(False) + #map log level to python log level - console is only added for Information or higher - if str_log_level == "Debug": - Telemetry.log_level = logging.DEBUG + if Telemetry.log_level == logging.DEBUG: set_debug(True) - handlers.append(logging.StreamHandler()) - elif str_log_level == "Trace": - Telemetry.log_level = logging.DEBUG - handlers.append(logging.StreamHandler()) set_verbose(True) - elif str_log_level == "Information": - Telemetry.log_level = logging.INFO handlers.append(logging.StreamHandler()) - set_debug(False) - elif str_log_level == "Warning": - set_debug(False) - Telemetry.log_level = logging.WARNING - elif str_log_level == "Error": - set_debug(False) - Telemetry.log_level = logging.ERROR - elif str_log_level == "Critical": - set_debug(False) - Telemetry.log_level = logging.CRITICAL - else: - set_debug(False) - Telemetry.log_level = logging.NOTSET #Logging configuration LOGGING = { @@ -200,7 +218,7 @@ def configure_logging(config: Configuration): }, 'loggers': { 'azure': { # Adjust the logger name accordingly - 'level': Telemetry.log_level, # Set to WARNING or higher + 'level': Telemetry.azure_log_level, "class": "opentelemetry.sdk._logs.LoggingHandler", 'filters': ['exclude_trace_logs'] },