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 22 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. import json
  2. import logging
  3. from datetime import datetime
  4. from pathlib import Path
  5. from typing import Callable, Dict, List, Literal, Optional, Union
  6. import aiofiles
  7. import yaml
  8. from autogen_agentchat.agents import AssistantAgent, UserProxyAgent
  9. from autogen_agentchat.conditions import (
  10. ExternalTermination,
  11. HandoffTermination,
  12. MaxMessageTermination,
  13. SourceMatchTermination,
  14. StopMessageTermination,
  15. TextMentionTermination,
  16. TimeoutTermination,
  17. TokenUsageTermination,
  18. )
  19. from autogen_agentchat.teams import MagenticOneGroupChat, RoundRobinGroupChat, SelectorGroupChat
  20. from autogen_core.tools import FunctionTool
  21. from autogen_ext.agents.file_surfer import FileSurfer
  22. from autogen_ext.agents.magentic_one import MagenticOneCoderAgent
  23. from autogen_ext.agents.web_surfer import MultimodalWebSurfer
  24. from autogen_ext.models.openai import AzureOpenAIChatCompletionClient, OpenAIChatCompletionClient
  25. from ..datamodel.types import (
  26. AgentConfig,
  27. AgentTypes,
  28. AssistantAgentConfig,
  29. AzureOpenAIModelConfig,
  30. CombinationTerminationConfig,
  31. ComponentConfig,
  32. ComponentConfigInput,
  33. ComponentTypes,
  34. MagenticOneTeamConfig,
  35. MaxMessageTerminationConfig,
  36. ModelConfig,
  37. ModelTypes,
  38. MultimodalWebSurferAgentConfig,
  39. OpenAIModelConfig,
  40. RoundRobinTeamConfig,
  41. SelectorTeamConfig,
  42. TeamConfig,
  43. TeamTypes,
  44. TerminationConfig,
  45. TerminationTypes,
  46. TextMentionTerminationConfig,
  47. ToolConfig,
  48. ToolTypes,
  49. UserProxyAgentConfig,
  50. )
  51. from ..utils.utils import Version
  52. logger = logging.getLogger(__name__)
  53. TeamComponent = Union[RoundRobinGroupChat, SelectorGroupChat, MagenticOneGroupChat]
  54. AgentComponent = Union[AssistantAgent, MultimodalWebSurfer, UserProxyAgent, FileSurfer, MagenticOneCoderAgent]
  55. ModelComponent = Union[OpenAIChatCompletionClient, AzureOpenAIChatCompletionClient]
  56. ToolComponent = Union[FunctionTool] # Will grow with more tool types
  57. TerminationComponent = Union[
  58. MaxMessageTermination,
  59. StopMessageTermination,
  60. TextMentionTermination,
  61. TimeoutTermination,
  62. ExternalTermination,
  63. TokenUsageTermination,
  64. HandoffTermination,
  65. SourceMatchTermination,
  66. StopMessageTermination,
  67. ]
  68. Component = Union[TeamComponent, AgentComponent, ModelComponent, ToolComponent, TerminationComponent]
  69. ReturnType = Literal["object", "dict", "config"]
  70. DEFAULT_SELECTOR_PROMPT = """You are in a role play game. The following roles are available:
  71. {roles}.
  72. Read the following conversation. Then select the next role from {participants} to play. Only return the role.
  73. {history}
  74. Read the above conversation. Then select the next role from {participants} to play. Only return the role.
  75. """
  76. CONFIG_RETURN_TYPES = Literal["object", "dict", "config"]
  77. class ComponentFactory:
  78. """Creates and manages agent components with versioned configuration loading"""
  79. SUPPORTED_VERSIONS = {
  80. ComponentTypes.TEAM: ["1.0.0"],
  81. ComponentTypes.AGENT: ["1.0.0"],
  82. ComponentTypes.MODEL: ["1.0.0"],
  83. ComponentTypes.TOOL: ["1.0.0"],
  84. ComponentTypes.TERMINATION: ["1.0.0"],
  85. }
  86. def __init__(self):
  87. self._model_cache: Dict[str, ModelComponent] = {}
  88. self._tool_cache: Dict[str, FunctionTool] = {}
  89. self._last_cache_clear = datetime.now()
  90. async def load(
  91. self, component: ComponentConfigInput, input_func: Optional[Callable] = None, return_type: ReturnType = "object"
  92. ) -> Union[Component, dict, ComponentConfig]:
  93. """
  94. Universal loader for any component type
  95. Args:
  96. component: Component configuration (file path, dict, or ComponentConfig)
  97. input_func: Optional callable for user input handling
  98. return_type: Type of return value ('object', 'dict', or 'config')
  99. Returns:
  100. Component instance, config dict, or ComponentConfig based on return_type
  101. """
  102. try:
  103. # Load and validate config
  104. if isinstance(component, (str, Path)):
  105. component_dict = await self._load_from_file(component)
  106. config = self._dict_to_config(component_dict)
  107. elif isinstance(component, dict):
  108. config = self._dict_to_config(component)
  109. else:
  110. config = component
  111. # Validate version
  112. if not self._is_version_supported(config.component_type, config.version):
  113. raise ValueError(
  114. f"Unsupported version {config.version} for "
  115. f"component type {config.component_type}. "
  116. f"Supported versions: {self.SUPPORTED_VERSIONS[config.component_type]}"
  117. )
  118. # Return early if dict or config requested
  119. if return_type == "dict":
  120. return config.model_dump()
  121. elif return_type == "config":
  122. return config
  123. # Otherwise create and return component instance
  124. handlers = {
  125. ComponentTypes.TEAM: lambda c: self.load_team(c, input_func),
  126. ComponentTypes.AGENT: lambda c: self.load_agent(c, input_func),
  127. ComponentTypes.MODEL: self.load_model,
  128. ComponentTypes.TOOL: self.load_tool,
  129. ComponentTypes.TERMINATION: self.load_termination,
  130. }
  131. handler = handlers.get(config.component_type)
  132. if not handler:
  133. raise ValueError(f"Unknown component type: {config.component_type}")
  134. return await handler(config)
  135. except Exception as e:
  136. logger.error(f"Failed to load component: {str(e)}")
  137. raise
  138. async def load_directory(
  139. self, directory: Union[str, Path], return_type: ReturnType = "object"
  140. ) -> List[Union[Component, dict, ComponentConfig]]:
  141. """
  142. Import all component configurations from a directory.
  143. """
  144. components = []
  145. try:
  146. directory = Path(directory)
  147. # Using Path.iterdir() instead of os.listdir
  148. for path in list(directory.glob("*")):
  149. if path.suffix.lower().endswith((".json", ".yaml", ".yml")):
  150. try:
  151. component = await self.load(path, return_type=return_type)
  152. components.append(component)
  153. except Exception as e:
  154. logger.info(f"Failed to load component: {str(e)}, {path}")
  155. return components
  156. except Exception as e:
  157. logger.info(f"Failed to load directory: {str(e)}")
  158. return components
  159. def _dict_to_config(self, config_dict: dict) -> ComponentConfig:
  160. """Convert dictionary to appropriate config type based on component_type and type discriminator"""
  161. if "component_type" not in config_dict:
  162. raise ValueError("component_type is required in configuration")
  163. component_type = ComponentTypes(config_dict["component_type"])
  164. # Define mapping structure
  165. type_mappings = {
  166. ComponentTypes.MODEL: {
  167. "discriminator": "model_type",
  168. ModelTypes.OPENAI.value: OpenAIModelConfig,
  169. ModelTypes.AZUREOPENAI.value: AzureOpenAIModelConfig,
  170. },
  171. ComponentTypes.AGENT: {
  172. "discriminator": "agent_type",
  173. AgentTypes.ASSISTANT.value: AssistantAgentConfig,
  174. AgentTypes.USERPROXY.value: UserProxyAgentConfig,
  175. AgentTypes.MULTIMODAL_WEBSURFER.value: MultimodalWebSurferAgentConfig,
  176. },
  177. ComponentTypes.TEAM: {
  178. "discriminator": "team_type",
  179. TeamTypes.ROUND_ROBIN.value: RoundRobinTeamConfig,
  180. TeamTypes.SELECTOR.value: SelectorTeamConfig,
  181. TeamTypes.MAGENTIC_ONE.value: MagenticOneTeamConfig,
  182. },
  183. ComponentTypes.TOOL: ToolConfig,
  184. ComponentTypes.TERMINATION: {
  185. "discriminator": "termination_type",
  186. TerminationTypes.MAX_MESSAGES.value: MaxMessageTerminationConfig,
  187. TerminationTypes.TEXT_MENTION.value: TextMentionTerminationConfig,
  188. TerminationTypes.COMBINATION.value: CombinationTerminationConfig,
  189. },
  190. }
  191. mapping = type_mappings.get(component_type)
  192. if not mapping:
  193. raise ValueError(f"Unknown component type: {component_type}")
  194. # Handle simple cases (no discriminator)
  195. if isinstance(mapping, type):
  196. return mapping(**config_dict)
  197. # Get discriminator field value
  198. discriminator = mapping["discriminator"]
  199. if discriminator not in config_dict:
  200. raise ValueError(f"Missing {discriminator} in configuration")
  201. type_value = config_dict[discriminator]
  202. config_class = mapping.get(type_value)
  203. if not config_class:
  204. raise ValueError(f"Unknown {discriminator}: {type_value}")
  205. return config_class(**config_dict)
  206. async def load_termination(self, config: TerminationConfig) -> TerminationComponent:
  207. """Create termination condition instance from configuration."""
  208. try:
  209. if config.termination_type == TerminationTypes.COMBINATION:
  210. if not config.conditions or len(config.conditions) < 2:
  211. raise ValueError("Combination termination requires at least 2 conditions")
  212. if not config.operator:
  213. raise ValueError("Combination termination requires an operator (and/or)")
  214. # Load first two conditions
  215. conditions = [await self.load_termination(cond) for cond in config.conditions[:2]]
  216. result = conditions[0] & conditions[1] if config.operator == "and" else conditions[0] | conditions[1]
  217. # Process remaining conditions if any
  218. for condition in config.conditions[2:]:
  219. next_condition = await self.load_termination(condition)
  220. result = result & next_condition if config.operator == "and" else result | next_condition
  221. return result
  222. elif config.termination_type == TerminationTypes.MAX_MESSAGES:
  223. if config.max_messages is None:
  224. raise ValueError("max_messages parameter required for MaxMessageTermination")
  225. return MaxMessageTermination(max_messages=config.max_messages)
  226. elif config.termination_type == TerminationTypes.STOP_MESSAGE:
  227. return StopMessageTermination()
  228. elif config.termination_type == TerminationTypes.TEXT_MENTION:
  229. if not config.text:
  230. raise ValueError("text parameter required for TextMentionTermination")
  231. return TextMentionTermination(text=config.text)
  232. else:
  233. raise ValueError(f"Unsupported termination type: {config.termination_type}")
  234. except Exception as e:
  235. logger.error(f"Failed to create termination condition: {str(e)}")
  236. raise ValueError(f"Termination condition creation failed: {str(e)}") from e
  237. async def load_team(self, config: TeamConfig, input_func: Optional[Callable] = None) -> TeamComponent:
  238. """Create team instance from configuration."""
  239. try:
  240. # Load participants (agents) with input_func
  241. participants = []
  242. for participant in config.participants:
  243. agent = await self.load(participant, input_func=input_func)
  244. participants.append(agent)
  245. # Load termination condition if specified
  246. termination = None
  247. if config.termination_condition:
  248. termination = await self.load(config.termination_condition)
  249. # Create team based on type
  250. if config.team_type == TeamTypes.ROUND_ROBIN:
  251. return RoundRobinGroupChat(participants=participants, termination_condition=termination)
  252. elif config.team_type == TeamTypes.SELECTOR:
  253. model_client = await self.load(config.model_client)
  254. if not model_client:
  255. raise ValueError("SelectorGroupChat requires a model_client")
  256. selector_prompt = config.selector_prompt if config.selector_prompt else DEFAULT_SELECTOR_PROMPT
  257. return SelectorGroupChat(
  258. participants=participants,
  259. model_client=model_client,
  260. termination_condition=termination,
  261. selector_prompt=selector_prompt,
  262. )
  263. elif config.team_type == TeamTypes.MAGENTIC_ONE:
  264. model_client = await self.load(config.model_client)
  265. if not model_client:
  266. raise ValueError("MagenticOneGroupChat requires a model_client")
  267. return MagenticOneGroupChat(
  268. participants=participants,
  269. model_client=model_client,
  270. termination_condition=termination if termination is not None else None,
  271. max_turns=config.max_turns if config.max_turns is not None else 20,
  272. )
  273. else:
  274. raise ValueError(f"Unsupported team type: {config.team_type}")
  275. except Exception as e:
  276. logger.error(f"Failed to create team {config.name}: {str(e)}")
  277. raise ValueError(f"Team creation failed: {str(e)}") from e
  278. async def load_agent(self, config: AgentConfig, input_func: Optional[Callable] = None) -> AgentComponent:
  279. """Create agent instance from configuration."""
  280. model_client = None
  281. system_message = None
  282. tools = []
  283. if hasattr(config, "system_message") and config.system_message:
  284. system_message = config.system_message
  285. if hasattr(config, "model_client") and config.model_client:
  286. model_client = await self.load(config.model_client)
  287. if hasattr(config, "tools") and config.tools:
  288. for tool_config in config.tools:
  289. tool = await self.load(tool_config)
  290. tools.append(tool)
  291. try:
  292. if config.agent_type == AgentTypes.USERPROXY:
  293. return UserProxyAgent(
  294. name=config.name,
  295. description=config.description or "A human user",
  296. input_func=input_func, # Pass through to UserProxyAgent
  297. )
  298. elif config.agent_type == AgentTypes.ASSISTANT:
  299. system_message = config.system_message if config.system_message else "You are a helpful assistant"
  300. return AssistantAgent(
  301. name=config.name,
  302. description=config.description or "A helpful assistant",
  303. model_client=model_client,
  304. tools=tools,
  305. system_message=system_message,
  306. )
  307. elif config.agent_type == AgentTypes.MULTIMODAL_WEBSURFER:
  308. return MultimodalWebSurfer(
  309. name=config.name,
  310. model_client=model_client,
  311. headless=config.headless if config.headless is not None else True,
  312. debug_dir=config.logs_dir if config.logs_dir is not None else None,
  313. downloads_folder=config.logs_dir if config.logs_dir is not None else None,
  314. to_save_screenshots=config.to_save_screenshots if config.to_save_screenshots is not None else False,
  315. use_ocr=config.use_ocr if config.use_ocr is not None else False,
  316. animate_actions=config.animate_actions if config.animate_actions is not None else False,
  317. )
  318. elif config.agent_type == AgentTypes.FILE_SURFER:
  319. return FileSurfer(
  320. name=config.name,
  321. model_client=model_client,
  322. )
  323. elif config.agent_type == AgentTypes.MAGENTIC_ONE_CODER:
  324. return MagenticOneCoderAgent(
  325. name=config.name,
  326. model_client=model_client,
  327. )
  328. else:
  329. raise ValueError(f"Unsupported agent type: {config.agent_type}")
  330. except Exception as e:
  331. logger.error(f"Failed to create agent {config.name}: {str(e)}")
  332. raise ValueError(f"Agent creation failed: {str(e)}") from e
  333. async def load_model(self, config: ModelConfig) -> ModelComponent:
  334. """Create model instance from configuration."""
  335. try:
  336. # Check cache first
  337. cache_key = str(config.model_dump())
  338. if cache_key in self._model_cache:
  339. logger.debug(f"Using cached model for {config.model}")
  340. return self._model_cache[cache_key]
  341. if config.model_type == ModelTypes.OPENAI:
  342. args = {
  343. "model": config.model,
  344. "api_key": config.api_key,
  345. "base_url": config.base_url,
  346. }
  347. if hasattr(config, "model_capabilities") and config.model_capabilities is not None:
  348. args["model_capabilities"] = config.model_capabilities
  349. model = OpenAIChatCompletionClient(**args)
  350. self._model_cache[cache_key] = model
  351. return model
  352. elif config.model_type == ModelTypes.AZUREOPENAI:
  353. model = AzureOpenAIChatCompletionClient(
  354. azure_deployment=config.azure_deployment,
  355. model=config.model,
  356. api_version=config.api_version,
  357. azure_endpoint=config.azure_endpoint,
  358. api_key=config.api_key,
  359. )
  360. self._model_cache[cache_key] = model
  361. return model
  362. else:
  363. raise ValueError(f"Unsupported model type: {config.model_type}")
  364. except Exception as e:
  365. logger.error(f"Failed to create model {config.model}: {str(e)}")
  366. raise ValueError(f"Model creation failed: {str(e)}") from e
  367. async def load_tool(self, config: ToolConfig) -> ToolComponent:
  368. """Create tool instance from configuration."""
  369. try:
  370. # Validate required fields
  371. if not all([config.name, config.description, config.content, config.tool_type]):
  372. raise ValueError("Tool configuration missing required fields")
  373. # Check cache first
  374. cache_key = str(config.model_dump())
  375. if cache_key in self._tool_cache:
  376. logger.debug(f"Using cached tool '{config.name}'")
  377. return self._tool_cache[cache_key]
  378. if config.tool_type == ToolTypes.PYTHON_FUNCTION:
  379. tool = FunctionTool(
  380. name=config.name, description=config.description, func=self._func_from_string(config.content)
  381. )
  382. self._tool_cache[cache_key] = tool
  383. return tool
  384. else:
  385. raise ValueError(f"Unsupported tool type: {config.tool_type}")
  386. except Exception as e:
  387. logger.error(f"Failed to create tool '{config.name}': {str(e)}")
  388. raise
  389. async def _load_from_file(self, path: Union[str, Path]) -> dict:
  390. """Load configuration from JSON or YAML file."""
  391. path = Path(path)
  392. if not path.exists():
  393. raise FileNotFoundError(f"Config file not found: {path}")
  394. try:
  395. async with aiofiles.open(path) as f:
  396. content = await f.read()
  397. if path.suffix == ".json":
  398. return json.loads(content)
  399. elif path.suffix in (".yml", ".yaml"):
  400. return yaml.safe_load(content)
  401. else:
  402. raise ValueError(f"Unsupported file format: {path.suffix}")
  403. except Exception as e:
  404. raise ValueError(f"Failed to load file {path}: {str(e)}") from e
  405. def _func_from_string(self, content: str) -> callable:
  406. """Convert function string to callable."""
  407. try:
  408. namespace = {}
  409. exec(content, namespace)
  410. for item in namespace.values():
  411. if callable(item) and not isinstance(item, type):
  412. return item
  413. raise ValueError("No function found in provided code")
  414. except Exception as e:
  415. raise ValueError(f"Failed to create function: {str(e)}") from e
  416. def _is_version_supported(self, component_type: ComponentTypes, ver: str) -> bool:
  417. """Check if version is supported for component type."""
  418. try:
  419. version = Version(ver)
  420. supported = [Version(v) for v in self.SUPPORTED_VERSIONS[component_type]]
  421. return any(version == v for v in supported)
  422. except ValueError:
  423. return False
  424. async def cleanup(self) -> None:
  425. """Cleanup resources and clear caches."""
  426. for model in self._model_cache.values():
  427. if hasattr(model, "cleanup"):
  428. await model.cleanup()
  429. for tool in self._tool_cache.values():
  430. if hasattr(tool, "cleanup"):
  431. await tool.cleanup()
  432. self._model_cache.clear()
  433. self._tool_cache.clear()
  434. self._last_cache_clear = datetime.now()
  435. logger.info("Cleared all component caches")