Skip to content

Commit

Permalink
Merge branch 'fix/chore-fix' into dev/plugin-deploy
Browse files Browse the repository at this point in the history
  • Loading branch information
Yeuoly committed Dec 9, 2024
2 parents 54f03c5 + 4ccd571 commit ff81d12
Show file tree
Hide file tree
Showing 15 changed files with 446 additions and 19 deletions.
10 changes: 9 additions & 1 deletion api/core/agent/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from pydantic import BaseModel

from core.tools.entities.tool_entities import ToolProviderType
from core.tools.entities.tool_entities import ToolInvokeMessage, ToolProviderType


class AgentToolEntity(BaseModel):
Expand Down Expand Up @@ -83,3 +83,11 @@ class Strategy(Enum):
prompt: Optional[AgentPromptEntity] = None
tools: Optional[list[AgentToolEntity]] = None
max_iteration: int = 5


class AgentInvokeMessage(ToolInvokeMessage):
"""
Agent Invoke Message.
"""

pass
42 changes: 42 additions & 0 deletions api/core/agent/plugin_entities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from typing import Optional

from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator

from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_entities import ToolIdentity, ToolParameter, ToolProviderIdentity


class AgentProviderIdentity(ToolProviderIdentity):
pass


class AgentParameter(ToolParameter):
pass


class AgentProviderEntity(BaseModel):
identity: AgentProviderIdentity
plugin_id: Optional[str] = Field(None, description="The id of the plugin")


class AgentIdentity(ToolIdentity):
pass


class AgentStrategyEntity(BaseModel):
identity: AgentIdentity
parameters: list[AgentParameter] = Field(default_factory=list)
description: I18nObject = Field(..., description="The description of the agent strategy")
output_schema: Optional[dict] = None

# pydantic configs
model_config = ConfigDict(protected_namespaces=())

@field_validator("parameters", mode="before")
@classmethod
def set_parameters(cls, v, validation_info: ValidationInfo) -> list[AgentParameter]:
return v or []


class AgentProviderEntityWithPlugin(AgentProviderEntity):
strategies: list[AgentStrategyEntity] = Field(default_factory=list)
41 changes: 41 additions & 0 deletions api/core/agent/strategy/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from abc import ABC, abstractmethod
from typing import Any, Generator, Optional, Sequence

from core.agent.entities import AgentInvokeMessage
from core.agent.plugin_entities import AgentParameter


class BaseAgentStrategy(ABC):
"""
Agent Strategy
"""

def invoke(
self,
params: dict[str, Any],
user_id: str,
conversation_id: Optional[str] = None,
app_id: Optional[str] = None,
message_id: Optional[str] = None,
) -> Generator[AgentInvokeMessage, None, None]:
"""
Invoke the agent strategy.
"""
yield from self._invoke(params, user_id, conversation_id, app_id, message_id)

def get_parameters(self) -> Sequence[AgentParameter]:
"""
Get the parameters for the agent strategy.
"""
return []

@abstractmethod
def _invoke(
self,
params: dict[str, Any],
user_id: str,
conversation_id: Optional[str] = None,
app_id: Optional[str] = None,
message_id: Optional[str] = None,
) -> Generator[AgentInvokeMessage, None, None]:
pass
52 changes: 52 additions & 0 deletions api/core/agent/strategy/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from typing import Any, Generator, Optional, Sequence

from core.agent.entities import AgentInvokeMessage
from core.agent.plugin_entities import AgentParameter, AgentStrategyEntity
from core.agent.strategy.base import BaseAgentStrategy
from core.plugin.manager.agent import PluginAgentManager
from core.tools.plugin_tool.tool import PluginTool


class PluginAgentStrategy(BaseAgentStrategy):
"""
Agent Strategy
"""

tenant_id: str
plugin_unique_identifier: str
declaration: AgentStrategyEntity

def __init__(self, tenant_id: str, plugin_unique_identifier: str, declaration: AgentStrategyEntity):
self.tenant_id = tenant_id
self.plugin_unique_identifier = plugin_unique_identifier
self.declaration = declaration

def get_parameters(self) -> Sequence[AgentParameter]:
return self.declaration.parameters

def _invoke(
self,
params: dict[str, Any],
user_id: str,
conversation_id: Optional[str] = None,
app_id: Optional[str] = None,
message_id: Optional[str] = None,
) -> Generator[AgentInvokeMessage, None, None]:
"""
Invoke the agent strategy.
"""
manager = PluginAgentManager()

# convert agent parameters with File type to PluginFileEntity
params = PluginTool._transform_image_parameters(params)

yield from manager.invoke(
tenant_id=self.tenant_id,
user_id=user_id,
agent_provider=self.declaration.identity.provider,
agent_strategy=self.declaration.identity.name,
agent_params=params,
conversation_id=conversation_id,
app_id=app_id,
message_id=message_id,
)
8 changes: 8 additions & 0 deletions api/core/plugin/entities/plugin_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from pydantic import BaseModel, ConfigDict, Field

from core.agent.plugin_entities import AgentProviderEntityWithPlugin
from core.model_runtime.entities.model_entities import AIModelEntity
from core.model_runtime.entities.provider_entities import ProviderEntity
from core.plugin.entities.base import BasePluginEntity
Expand Down Expand Up @@ -46,6 +47,13 @@ class PluginToolProviderEntity(BaseModel):
declaration: ToolProviderEntityWithPlugin


class PluginAgentProviderEntity(BaseModel):
provider: str
plugin_unique_identifier: str
plugin_id: str
declaration: AgentProviderEntityWithPlugin


class PluginBasicBooleanResponse(BaseModel):
"""
Basic boolean response from plugin daemon.
Expand Down
109 changes: 109 additions & 0 deletions api/core/plugin/manager/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from collections.abc import Generator
from typing import Any, Optional

from core.agent.entities import AgentInvokeMessage
from core.plugin.entities.plugin import GenericProviderID
from core.plugin.entities.plugin_daemon import (
PluginAgentProviderEntity,
)
from core.plugin.manager.base import BasePluginManager


class PluginAgentManager(BasePluginManager):
def fetch_agent_providers(self, tenant_id: str) -> list[PluginAgentProviderEntity]:
"""
Fetch agent providers for the given tenant.
"""

def transformer(json_response: dict[str, Any]) -> dict:
for provider in json_response.get("data", []):
declaration = provider.get("declaration", {}) or {}
provider_name = declaration.get("identity", {}).get("name")
for tool in declaration.get("tools", []):
tool["identity"]["provider"] = provider_name

return json_response

response = self._request_with_plugin_daemon_response(
"GET",
f"plugin/{tenant_id}/management/agents",
list[PluginAgentProviderEntity],
params={"page": 1, "page_size": 256},
transformer=transformer,
)

for provider in response:
provider.declaration.identity.name = f"{provider.plugin_id}/{provider.declaration.identity.name}"

# override the provider name for each tool to plugin_id/provider_name
for strategy in provider.declaration.strategies:
strategy.identity.provider = provider.declaration.identity.name

return response

def fetch_agent_provider(self, tenant_id: str, provider: str) -> PluginAgentProviderEntity:
"""
Fetch tool provider for the given tenant and plugin.
"""
agent_provider_id = GenericProviderID(provider)

def transformer(json_response: dict[str, Any]) -> dict:
for strategy in json_response.get("data", {}).get("declaration", {}).get("strategies", []):
strategy["identity"]["provider"] = agent_provider_id.provider_name

return json_response

response = self._request_with_plugin_daemon_response(
"GET",
f"plugin/{tenant_id}/management/agent",
PluginAgentProviderEntity,
params={"provider": agent_provider_id.provider_name, "plugin_id": agent_provider_id.plugin_id},
transformer=transformer,
)

response.declaration.identity.name = f"{response.plugin_id}/{response.declaration.identity.name}"

# override the provider name for each tool to plugin_id/provider_name
for strategy in response.declaration.strategies:
strategy.identity.provider = response.declaration.identity.name

return response

def invoke(
self,
tenant_id: str,
user_id: str,
agent_provider: str,
agent_strategy: str,
agent_params: dict[str, Any],
conversation_id: Optional[str] = None,
app_id: Optional[str] = None,
message_id: Optional[str] = None,
) -> Generator[AgentInvokeMessage, None, None]:
"""
Invoke the agent with the given tenant, user, plugin, provider, name and parameters.
"""

agent_provider_id = GenericProviderID(agent_provider)

response = self._request_with_plugin_daemon_response_stream(
"POST",
f"plugin/{tenant_id}/dispatch/agent/invoke",
AgentInvokeMessage,
data={
"user_id": user_id,
"conversation_id": conversation_id,
"app_id": app_id,
"message_id": message_id,
"data": {
"provider": agent_provider_id.provider_name,
"strategy": agent_strategy,
"agent_params": agent_params,
},
},
headers={
"X-Plugin-ID": agent_provider_id.plugin_id,
"Content-Type": "application/json",
},
)
return response
Empty file.
37 changes: 21 additions & 16 deletions api/core/tools/plugin_tool/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,14 @@ def __init__(
def tool_provider_type(self) -> ToolProviderType:
return ToolProviderType.PLUGIN

def _invoke(
self,
user_id: str,
tool_parameters: dict[str, Any],
conversation_id: Optional[str] = None,
app_id: Optional[str] = None,
message_id: Optional[str] = None,
) -> Generator[ToolInvokeMessage, None, None]:
manager = PluginToolManager()

# convert tool parameters with File type to PluginFileEntity
for parameter_name, parameter in tool_parameters.items():
@classmethod
def _transform_image_parameters(cls, parameters: dict[str, Any]) -> dict[str, Any]:
for parameter_name, parameter in parameters.items():
if isinstance(parameter, File):
url = parameter.generate_url()
if url is None:
raise ValueError(f"File {parameter.id} does not have a valid URL")
tool_parameters[parameter_name] = PluginFileEntity(
parameters[parameter_name] = PluginFileEntity(
url=url,
mime_type=parameter.mime_type,
type=parameter.type,
Expand All @@ -52,13 +43,13 @@ def _invoke(
size=parameter.size,
).model_dump()
elif isinstance(parameter, list) and all(isinstance(p, File) for p in parameter):
tool_parameters[parameter_name] = []
parameters[parameter_name] = []
for p in parameter:
assert isinstance(p, File)
url = p.generate_url()
if url is None:
raise ValueError(f"File {p.id} does not have a valid URL")
tool_parameters[parameter_name].append(
parameters[parameter_name].append(
PluginFileEntity(
url=url,
mime_type=p.mime_type,
Expand All @@ -68,8 +59,22 @@ def _invoke(
size=p.size,
).model_dump()
)
return parameters

def _invoke(
self,
user_id: str,
tool_parameters: dict[str, Any],
conversation_id: Optional[str] = None,
app_id: Optional[str] = None,
message_id: Optional[str] = None,
) -> Generator[ToolInvokeMessage, None, None]:
manager = PluginToolManager()

# convert tool parameters with File type to PluginFileEntity
tool_parameters = self._transform_image_parameters(tool_parameters)

return manager.invoke(
yield from manager.invoke(
tenant_id=self.tenant_id,
user_id=user_id,
tool_provider=self.entity.identity.provider,
Expand Down
3 changes: 3 additions & 0 deletions api/core/workflow/nodes/agent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .agent_node import AgentNode

__all__ = ["AgentNode"]
Loading

0 comments on commit ff81d12

Please sign in to comment.