You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

component_factory.py 16 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import os
  2. from pathlib import Path
  3. from typing import Callable, List, Literal, Union, Optional, Dict, Any, Type
  4. from datetime import datetime
  5. import json
  6. from autogen_agentchat.task import MaxMessageTermination, TextMentionTermination, StopMessageTermination
  7. import yaml
  8. import logging
  9. from packaging import version
  10. from ..datamodel import (
  11. TeamConfig, AgentConfig, ModelConfig, ToolConfig,
  12. TeamTypes, AgentTypes, ModelTypes, ToolTypes,
  13. ComponentType, ComponentConfig, ComponentConfigInput, TerminationConfig, TerminationTypes, Response
  14. )
  15. from ..components import UserProxyAgent
  16. from autogen_agentchat.agents import AssistantAgent
  17. from autogen_agentchat.teams import RoundRobinGroupChat, SelectorGroupChat
  18. from autogen_ext.models import OpenAIChatCompletionClient
  19. from autogen_core.components.tools import FunctionTool
  20. logger = logging.getLogger(__name__)
  21. # Type definitions for supported components
  22. TeamComponent = Union[RoundRobinGroupChat, SelectorGroupChat]
  23. AgentComponent = Union[AssistantAgent] # Will grow with more agent types
  24. # Will grow with more model types
  25. ModelComponent = Union[OpenAIChatCompletionClient]
  26. ToolComponent = Union[FunctionTool] # Will grow with more tool types
  27. TerminationComponent = Union[MaxMessageTermination,
  28. StopMessageTermination, TextMentionTermination]
  29. # Config type definitions
  30. Component = Union[TeamComponent, AgentComponent, ModelComponent, ToolComponent]
  31. ReturnType = Literal['object', 'dict', 'config']
  32. Component = Union[RoundRobinGroupChat, SelectorGroupChat,
  33. AssistantAgent, OpenAIChatCompletionClient, FunctionTool]
  34. DEFAULT_SELECTOR_PROMPT = """You are in a role play game. The following roles are available:
  35. {roles}.
  36. Read the following conversation. Then select the next role from {participants} to play. Only return the role.
  37. {history}
  38. Read the above conversation. Then select the next role from {participants} to play. Only return the role.
  39. """
  40. CONFIG_RETURN_TYPES = Literal['object', 'dict', 'config']
  41. class ComponentFactory:
  42. """Creates and manages agent components with versioned configuration loading"""
  43. SUPPORTED_VERSIONS = {
  44. ComponentType.TEAM: ["1.0.0"],
  45. ComponentType.AGENT: ["1.0.0"],
  46. ComponentType.MODEL: ["1.0.0"],
  47. ComponentType.TOOL: ["1.0.0"],
  48. ComponentType.TERMINATION: ["1.0.0"]
  49. }
  50. def __init__(self):
  51. self._model_cache: Dict[str, OpenAIChatCompletionClient] = {}
  52. self._tool_cache: Dict[str, FunctionTool] = {}
  53. self._last_cache_clear = datetime.now()
  54. async def load(
  55. self,
  56. component: ComponentConfigInput,
  57. input_func: Optional[Callable] = None,
  58. return_type: ReturnType = 'object'
  59. ) -> Union[Component, dict, ComponentConfig]:
  60. """
  61. Universal loader for any component type
  62. Args:
  63. component: Component configuration (file path, dict, or ComponentConfig)
  64. input_func: Optional callable for user input handling
  65. return_type: Type of return value ('object', 'dict', or 'config')
  66. Returns:
  67. Component instance, config dict, or ComponentConfig based on return_type
  68. """
  69. try:
  70. # Load and validate config
  71. if isinstance(component, (str, Path)):
  72. component_dict = await self._load_from_file(component)
  73. config = self._dict_to_config(component_dict)
  74. elif isinstance(component, dict):
  75. config = self._dict_to_config(component)
  76. else:
  77. config = component
  78. # Validate version
  79. if not self._is_version_supported(config.component_type, config.version):
  80. raise ValueError(
  81. f"Unsupported version {config.version} for "
  82. f"component type {config.component_type}. "
  83. f"Supported versions: {self.SUPPORTED_VERSIONS[config.component_type]}"
  84. )
  85. # Return early if dict or config requested
  86. if return_type == 'dict':
  87. return config.model_dump()
  88. elif return_type == 'config':
  89. return config
  90. # Otherwise create and return component instance
  91. handlers = {
  92. ComponentType.TEAM: lambda c: self.load_team(c, input_func),
  93. ComponentType.AGENT: lambda c: self.load_agent(c, input_func),
  94. ComponentType.MODEL: self.load_model,
  95. ComponentType.TOOL: self.load_tool,
  96. ComponentType.TERMINATION: self.load_termination
  97. }
  98. handler = handlers.get(config.component_type)
  99. if not handler:
  100. raise ValueError(
  101. f"Unknown component type: {config.component_type}")
  102. return await handler(config)
  103. except Exception as e:
  104. logger.error(f"Failed to load component: {str(e)}")
  105. raise
  106. async def load_directory(self, directory: Union[str, Path], return_type: ReturnType = 'object') -> List[Union[Component, dict, ComponentConfig]]:
  107. """
  108. Import all component configurations from a directory.
  109. """
  110. components = []
  111. try:
  112. directory = Path(directory)
  113. # Using Path.iterdir() instead of os.listdir
  114. for path in list(directory.glob("*")):
  115. if path.suffix.lower().endswith(('.json', '.yaml', '.yml')):
  116. try:
  117. component = await self.load(path, return_type=return_type)
  118. components.append(component)
  119. except Exception as e:
  120. logger.info(
  121. f"Failed to load component: {str(e)}, {path}")
  122. return components
  123. except Exception as e:
  124. logger.info(f"Failed to load directory: {str(e)}")
  125. return components
  126. def _dict_to_config(self, config_dict: dict) -> ComponentConfig:
  127. """Convert dictionary to appropriate config type based on component_type"""
  128. if "component_type" not in config_dict:
  129. raise ValueError("component_type is required in configuration")
  130. config_types = {
  131. ComponentType.TEAM: TeamConfig,
  132. ComponentType.AGENT: AgentConfig,
  133. ComponentType.MODEL: ModelConfig,
  134. ComponentType.TOOL: ToolConfig,
  135. ComponentType.TERMINATION: TerminationConfig # Add mapping for termination
  136. }
  137. component_type = ComponentType(config_dict["component_type"])
  138. config_class = config_types.get(component_type)
  139. if not config_class:
  140. raise ValueError(f"Unknown component type: {component_type}")
  141. return config_class(**config_dict)
  142. async def load_termination(self, config: TerminationConfig) -> TerminationComponent:
  143. """Create termination condition instance from configuration."""
  144. try:
  145. if config.termination_type == TerminationTypes.MAX_MESSAGES:
  146. return MaxMessageTermination(max_messages=config.max_messages)
  147. elif config.termination_type == TerminationTypes.STOP_MESSAGE:
  148. return StopMessageTermination()
  149. elif config.termination_type == TerminationTypes.TEXT_MENTION:
  150. if not config.text:
  151. raise ValueError(
  152. "text parameter required for TextMentionTermination")
  153. return TextMentionTermination(text=config.text)
  154. else:
  155. raise ValueError(
  156. f"Unsupported termination type: {config.termination_type}")
  157. except Exception as e:
  158. logger.error(f"Failed to create termination condition: {str(e)}")
  159. raise ValueError(
  160. f"Termination condition creation failed: {str(e)}")
  161. async def load_team(
  162. self,
  163. config: TeamConfig,
  164. input_func: Optional[Callable] = None
  165. ) -> TeamComponent:
  166. """Create team instance from configuration."""
  167. try:
  168. # Load participants (agents) with input_func
  169. participants = []
  170. for participant in config.participants:
  171. agent = await self.load(participant, input_func=input_func)
  172. participants.append(agent)
  173. # Load model client if specified
  174. model_client = None
  175. if config.model_client:
  176. model_client = await self.load(config.model_client)
  177. # Load termination condition if specified
  178. termination = None
  179. if config.termination_condition:
  180. termination = await self.load(config.termination_condition)
  181. # Create team based on type
  182. if config.team_type == TeamTypes.ROUND_ROBIN:
  183. return RoundRobinGroupChat(
  184. participants=participants,
  185. termination_condition=termination
  186. )
  187. elif config.team_type == TeamTypes.SELECTOR:
  188. if not model_client:
  189. raise ValueError(
  190. "SelectorGroupChat requires a model_client")
  191. selector_prompt = config.selector_prompt if config.selector_prompt else DEFAULT_SELECTOR_PROMPT
  192. return SelectorGroupChat(
  193. participants=participants,
  194. model_client=model_client,
  195. termination_condition=termination,
  196. selector_prompt=selector_prompt
  197. )
  198. else:
  199. raise ValueError(f"Unsupported team type: {config.team_type}")
  200. except Exception as e:
  201. logger.error(f"Failed to create team {config.name}: {str(e)}")
  202. raise ValueError(f"Team creation failed: {str(e)}")
  203. async def load_agent(
  204. self,
  205. config: AgentConfig,
  206. input_func: Optional[Callable] = None
  207. ) -> AgentComponent:
  208. """Create agent instance from configuration."""
  209. try:
  210. # Load model client if specified
  211. model_client = None
  212. if config.model_client:
  213. model_client = await self.load(config.model_client)
  214. system_message = config.system_message if config.system_message else "You are a helpful assistant"
  215. # Load tools if specified
  216. tools = []
  217. if config.tools:
  218. for tool_config in config.tools:
  219. tool = await self.load(tool_config)
  220. tools.append(tool)
  221. if config.agent_type == AgentTypes.USERPROXY:
  222. return UserProxyAgent(
  223. name=config.name,
  224. description=config.description or "A human user",
  225. input_func=input_func # Pass through to UserProxyAgent
  226. )
  227. elif config.agent_type == AgentTypes.ASSISTANT:
  228. return AssistantAgent(
  229. name=config.name,
  230. description=config.description or "A helpful assistant",
  231. model_client=model_client,
  232. tools=tools,
  233. system_message=system_message
  234. )
  235. else:
  236. raise ValueError(
  237. f"Unsupported agent type: {config.agent_type}")
  238. except Exception as e:
  239. logger.error(f"Failed to create agent {config.name}: {str(e)}")
  240. raise ValueError(f"Agent creation failed: {str(e)}")
  241. async def load_model(self, config: ModelConfig) -> ModelComponent:
  242. """Create model instance from configuration."""
  243. try:
  244. # Check cache first
  245. cache_key = str(config.model_dump())
  246. if cache_key in self._model_cache:
  247. logger.debug(f"Using cached model for {config.model}")
  248. return self._model_cache[cache_key]
  249. if config.model_type == ModelTypes.OPENAI:
  250. model = OpenAIChatCompletionClient(
  251. model=config.model,
  252. api_key=config.api_key,
  253. base_url=config.base_url
  254. )
  255. self._model_cache[cache_key] = model
  256. return model
  257. else:
  258. raise ValueError(
  259. f"Unsupported model type: {config.model_type}")
  260. except Exception as e:
  261. logger.error(f"Failed to create model {config.model}: {str(e)}")
  262. raise ValueError(f"Model creation failed: {str(e)}")
  263. async def load_tool(self, config: ToolConfig) -> ToolComponent:
  264. """Create tool instance from configuration."""
  265. try:
  266. # Validate required fields
  267. if not all([config.name, config.description, config.content, config.tool_type]):
  268. raise ValueError("Tool configuration missing required fields")
  269. # Check cache first
  270. cache_key = str(config.model_dump())
  271. if cache_key in self._tool_cache:
  272. logger.debug(f"Using cached tool '{config.name}'")
  273. return self._tool_cache[cache_key]
  274. if config.tool_type == ToolTypes.PYTHON_FUNCTION:
  275. tool = FunctionTool(
  276. name=config.name,
  277. description=config.description,
  278. func=self._func_from_string(config.content)
  279. )
  280. self._tool_cache[cache_key] = tool
  281. return tool
  282. else:
  283. raise ValueError(f"Unsupported tool type: {config.tool_type}")
  284. except Exception as e:
  285. logger.error(f"Failed to create tool '{config.name}': {str(e)}")
  286. raise
  287. # Helper methods remain largely the same
  288. async def _load_from_file(self, path: Union[str, Path]) -> dict:
  289. """Load configuration from JSON or YAML file."""
  290. path = Path(path)
  291. if not path.exists():
  292. raise FileNotFoundError(f"Config file not found: {path}")
  293. try:
  294. with open(path) as f:
  295. if path.suffix == '.json':
  296. return json.load(f)
  297. elif path.suffix in ('.yml', '.yaml'):
  298. return yaml.safe_load(f)
  299. else:
  300. raise ValueError(f"Unsupported file format: {path.suffix}")
  301. except Exception as e:
  302. raise ValueError(f"Failed to load file {path}: {str(e)}")
  303. def _func_from_string(self, content: str) -> callable:
  304. """Convert function string to callable."""
  305. try:
  306. namespace = {}
  307. exec(content, namespace)
  308. for item in namespace.values():
  309. if callable(item) and not isinstance(item, type):
  310. return item
  311. raise ValueError("No function found in provided code")
  312. except Exception as e:
  313. raise ValueError(f"Failed to create function: {str(e)}")
  314. def _is_version_supported(self, component_type: ComponentType, ver: str) -> bool:
  315. """Check if version is supported for component type."""
  316. try:
  317. v = version.parse(ver)
  318. return ver in self.SUPPORTED_VERSIONS[component_type]
  319. except version.InvalidVersion:
  320. return False
  321. async def cleanup(self) -> None:
  322. """Cleanup resources and clear caches."""
  323. for model in self._model_cache.values():
  324. if hasattr(model, 'cleanup'):
  325. await model.cleanup()
  326. for tool in self._tool_cache.values():
  327. if hasattr(tool, 'cleanup'):
  328. await tool.cleanup()
  329. self._model_cache.clear()
  330. self._tool_cache.clear()
  331. self._last_cache_clear = datetime.now()
  332. logger.info("Cleared all component caches")