Repository: crewAIInc/crewAI
Pull request #2523 (2025-04-03 17:57:35)

Feat/byoa

joaomdmoura: Disclaimer: This review was made by a crew of AI Agents.

# Code Review Comment: OpenAI Agent Adapter Implementation

## Overview
The recent pull request implements an OpenAI Agent Adapter designed to integrate OpenAI Assistants with CrewAI. This implementation spans three commits and introduces the core adapter class along with tool conversion functionalities.

## Positive Aspects
1. **Separation of Concerns**: Excellent use of the adapter pattern aids in maintaining a clean architecture.
2. **Error Handling and Event Emission**: Comprehensive approach to error handling throughout the implementation, ensuring robust operations.
3. **Type Hints and Documentation**: Type hints and in-line documentation are well-handled, improving code readability and collaboration.
4. **Tool Conversion Logic**: Proper handling of tool conversions between CrewAI and OpenAI formats, maintaining compatibility.

## Issues and Recommendations

### 1. Inconsistent Error Handling
The `execute_task` method currently captures all exceptions generically, which could obscure specific issues. 

- **Current Implementation**:
    ```python
    except Exception as e:
        self._logger.log("error", f"Error executing OpenAI task: {str(e)}")
    ```

- **Improved Handling**:
   ```python
    except OpenAIError as e:
        self._logger.log("error", f"OpenAI API Error: {str(e)}")
    except ValidationError as e:
        self._logger.log("error", f"Tool validation error: {str(e)}")
    except Exception as e:
        self._logger.log("error", f"Unexpected error: {str(e)}")
    ```

### 2. Model Configuration Hardcoding
The default model is hardcoded, which can create challenges if changes are necessary in the future.

- **Current Implementation**:
    ```python
    def __init__(self, openai_agent: OpenAIAgent, model: str = "gpt-4o-mini", ...):
    ```

- **Recommended Improvement**:
    ```python
    from crewai.config import Settings

    def __init__(self, openai_agent: OpenAIAgent, model: str = Settings.default_openai_model, ...):
    ```

### 3. Tool Configuration Optimization
Optimizing the tool conversion process using caching can lead to significant performance enhancements.

- **Recommended Improvement with Caching**:
    ```python
    from functools import lru_cache

    @lru_cache(maxsize=128)
    def _convert_tools_to_openai_format(self, tools: Tuple[BaseTool, ...]) -> List[Tool]:
        ...
    ```

### 4. Missing Input Validation
Adding validation for critical parameters will ensure that only valid data is processed, avoiding potential runtime errors.

- **Validator for Tools**:
    ```python
    @validator("tools", pre=True)
    def validate_tools(cls, v):
        if v is not None and not isinstance(v, list):
            raise ValueError("Tools must be a list of BaseTool instances")
        return v
    ```

### 5. Debug Logging Cleanup
Transitioning from debug print statements to structured logging will enhance maintainability and traceability.

- **Recommended Use of Logging**:
    ```python
    import logging

    logger = logging.getLogger(__name__)

    class OpenAIAgentAdapter(BaseAgent, BaseModel):
        def _configure_tools(self, tools: List[BaseTool]) -> None:
            logger.debug(f"Converting {len(tools)} tools to OpenAI format")
            ...
    ```

### 6. Memory Management Considerations
It’s crucial to implement proper cleanup mechanisms for long-running operations to prevent memory leaks.

- **Cleaning Up Resources**:
    ```python
    def __del__(self):
        if hasattr(self, '_active_thread') and self._active_thread:
            ...
    ```

### 7. Testing Considerations
Add doctest examples to key methods to validate functionality and ensure correct usage.

- **Example for Method Testing**:
    ```python
    def sanitize_tool_name(self, name: str) -> str:
        """
        Examples:
            >>> adapter = OpenAIAgentAdapter(...)
            >>> adapter.sanitize_tool_name("My Cool Tool!")
            'my_cool_tool'
        """
    ```

## Security and Performance Considerations
- Ensure proper input sanitization for all user-facing parameters.
- Implement rates limiting for API interactions to prevent abuse.
- Introduce caching and batch processing for API calls to optimize performance.

## Conclusion
The OpenAI Agent Adapter encapsulates critical functionalities for integrating OpenAI capabilities within the CrewAI framework. By addressing the outlined recommendations, you can enhance the implementation's robustness, stability, and maintainability, further promoting efficient development practices moving forward. 

These adjustments will serve to enhance the overall performance and reliability of the OpenAI Agent Adapter while preserving its core functionalities.
lorenzejay: still todos: 

- [ ] docs
- [ ] tests

Patch:
diff --git a/docs/how-to/bring-your-own-agent.mdx b/docs/how-to/bring-your-own-agent.mdx
new file mode 100644
index 00000000..3a4e05b5
--- /dev/null
+++ b/docs/how-to/bring-your-own-agent.mdx
@@ -0,0 +1,443 @@
+---
+title: Bring your own agent
+description: Learn how to bring your own agents that work within a Crew.
+icon: robots
+---
+
+Interoperability is a core concept in CrewAI. This guide will show you how to bring your own agents that work within a Crew.
+
+
+## Adapter Guide for Bringing your own agents (Langgraph Agents, OpenAI Agents, etc...)
+We require 3 adapters to turn any agent from different frameworks to work within crew.
+
+1. BaseAgentAdapter
+2. BaseToolAdapter
+3. BaseConverter
+
+
+## BaseAgentAdapter
+This abstract class defines the common interface and functionality that all
+agent adapters must implement. It extends BaseAgent to maintain compatibility
+with the CrewAI framework while adding adapter-specific requirements.
+
+Required Methods:
+
+1. `def configure_tools`
+2. `def configure_structured_output`
+
+## Creating your own Adapter
+To integrate an agent from a different framework (e.g., LangGraph, Autogen, OpenAI Assistants) into CrewAI, you need to create a custom adapter by inheriting from `BaseAgentAdapter`. This adapter acts as a compatibility layer, translating between the CrewAI interfaces and the specific requirements of your external agent.
+
+Here's how you implement your custom adapter:
+
+1.  **Inherit from `BaseAgentAdapter`**:
+    ```python
+    from crewai.agents.agent_adapters.base_agent_adapter import BaseAgentAdapter
+    from crewai.tools import BaseTool
+    from typing import List, Optional, Any, Dict
+
+    class MyCustomAgentAdapter(BaseAgentAdapter):
+        # ... implementation details ...
+    ```
+
+2.  **Implement `__init__`**:
+    The constructor should call the parent class constructor `super().__init__(**kwargs)` and perform any initialization specific to your external agent. You can use the optional `agent_config` dictionary passed during CrewAI's `Agent` initialization to configure your adapter and the underlying agent.
+
+    ```python
+    def __init__(self, agent_config: Optional[Dict[str, Any]] = None, **kwargs: Any):
+        super().__init__(agent_config=agent_config, **kwargs)
+        # Initialize your external agent here, possibly using agent_config
+        # Example: self.external_agent = initialize_my_agent(agent_config)
+        print(f"Initializing MyCustomAgentAdapter with config: {agent_config}")
+    ```
+
+3.  **Implement `configure_tools`**:
+    This abstract method is crucial. It receives a list of CrewAI `BaseTool` instances. Your implementation must convert or adapt these tools into the format expected by your external agent framework. This might involve wrapping them, extracting specific attributes, or registering them with the external agent instance.
+
+    ```python
+    def configure_tools(self, tools: Optional[List[BaseTool]] = None) -> None:
+        if tools:
+            adapted_tools = []
+            for tool in tools:
+                # Adapt CrewAI BaseTool to the format your agent expects
+                # Example: adapted_tool = adapt_to_my_framework(tool)
+                # adapted_tools.append(adapted_tool)
+                pass # Replace with your actual adaptation logic
+
+            # Configure the external agent with the adapted tools
+            # Example: self.external_agent.set_tools(adapted_tools)
+            print(f"Configuring tools for MyCustomAgentAdapter: {adapted_tools}") # Placeholder
+        else:
+            # Handle the case where no tools are provided
+            # Example: self.external_agent.set_tools([])
+            print("No tools provided for MyCustomAgentAdapter.")
+    ```
+
+4.  **Implement `configure_structured_output`**:
+    This method is called when the CrewAI `Agent` is configured with structured output requirements (e.g., `output_json` or `output_pydantic`). Your adapter needs to ensure the external agent is set up to comply with these requirements. This might involve setting specific parameters on the external agent or ensuring its underlying model supports the requested format. If the external agent doesn't support structured output in a way compatible with CrewAI's expectations, you might need to handle the conversion or raise an appropriate error.
+
+    ```python
+    def configure_structured_output(self, structured_output: Any) -> None:
+        # Configure your external agent to produce output in the specified format
+        # Example: self.external_agent.set_output_format(structured_output)
+        self.adapted_structured_output = True # Signal that structured output is handled
+        print(f"Configuring structured output for MyCustomAgentAdapter: {structured_output}")
+    ```
+
+By implementing these methods, your `MyCustomAgentAdapter` will allow your custom agent implementation to function correctly within a CrewAI crew, interacting with tasks and tools seamlessly. Remember to replace the example comments and print statements with your actual adaptation logic specific to the external agent framework you are integrating.
+
+## BaseToolAdapter implementation
+The `BaseToolAdapter` class is responsible for converting CrewAI's native `BaseTool` objects into a format that your specific external agent framework can understand and utilize. Different agent frameworks (like LangGraph, OpenAI Assistants, etc.) have their own unique ways of defining and handling tools, and the `BaseToolAdapter` acts as the translator.
+
+Here's how you implement your custom tool adapter:
+
+1.  **Inherit from `BaseToolAdapter`**:
+    ```python
+    from crewai.agents.agent_adapters.base_tool_adapter import BaseToolAdapter
+    from crewai.tools import BaseTool
+    from typing import List, Any
+
+    class MyCustomToolAdapter(BaseToolAdapter):
+        # ... implementation details ...
+    ```
+
+2.  **Implement `configure_tools`**:
+    This is the core abstract method you must implement. It receives a list of CrewAI `BaseTool` instances provided to the agent. Your task is to iterate through this list, adapt each `BaseTool` into the format expected by your external framework, and store the converted tools in the `self.converted_tools` list (which is initialized in the base class constructor).
+
+    ```python
+    def configure_tools(self, tools: List[BaseTool]) -> None:
+        """Configure and convert CrewAI tools for the specific implementation."""
+        self.converted_tools = [] # Reset in case it's called multiple times
+        for tool in tools:
+            # Sanitize the tool name if required by the target framework
+            sanitized_name = self.sanitize_tool_name(tool.name)
+
+            # --- Your Conversion Logic Goes Here ---
+            # Example: Convert BaseTool to a dictionary format for LangGraph
+            # converted_tool = {
+            #     "name": sanitized_name,
+            #     "description": tool.description,
+            #     "parameters": tool.args_schema.schema() if tool.args_schema else {},
+            #     # Add any other framework-specific fields
+            # }
+
+            # Example: Convert BaseTool to an OpenAI function definition
+            # converted_tool = {
+            #     "type": "function",
+            #     "function": {
+            #         "name": sanitized_name,
+            #         "description": tool.description,
+            #         "parameters": tool.args_schema.schema() if tool.args_schema else {"type": "object", "properties": {}},
+            #     }
+            # }
+
+            # --- Replace above examples with your actual adaptation ---
+            converted_tool = self.adapt_tool_to_my_framework(tool, sanitized_name) # Placeholder
+
+            self.converted_tools.append(converted_tool)
+            print(f"Adapted tool '{tool.name}' to '{sanitized_name}' for MyCustomToolAdapter") # Placeholder
+
+        print(f"MyCustomToolAdapter finished configuring tools: {len(self.converted_tools)} adapted.") # Placeholder
+
+    # --- Helper method for adaptation (Example) ---
+    def adapt_tool_to_my_framework(self, tool: BaseTool, sanitized_name: str) -> Any:
+        # Replace this with the actual logic to convert a CrewAI BaseTool
+        # to the format needed by your specific external agent framework.
+        # This will vary greatly depending on the target framework.
+        adapted_representation = {
+            "framework_specific_name": sanitized_name,
+            "framework_specific_description": tool.description,
+            "inputs": tool.args_schema.schema() if tool.args_schema else None,
+            "implementation_reference": tool.run # Or however the framework needs to call it
+        }
+        # Also ensure the tool works both sync and async
+        async def async_tool_wrapper(*args, **kwargs):
+            output = tool.run(*args, **kwargs)
+            if inspect.isawaitable(output):
+                return await output
+            else:
+                return output
+
+        adapted_tool = MyFrameworkTool(
+            name=sanitized_name,
+            description=tool.description,
+            inputs=tool.args_schema.schema() if tool.args_schema else None,
+            implementation_reference=async_tool_wrapper
+        )
+        
+        return adapted_representation
+
+    ```
+
+3.  **Using the Adapter**:
+    Typically, you would instantiate your `MyCustomToolAdapter` within your `MyCustomAgentAdapter`'s `configure_tools` method and use it to process the tools before configuring your external agent.
+
+    ```python
+    # Inside MyCustomAgentAdapter.configure_tools
+    def configure_tools(self, tools: Optional[List[BaseTool]] = None) -> None:
+        if tools:
+            tool_adapter = MyCustomToolAdapter() # Instantiate your tool adapter
+            tool_adapter.configure_tools(tools)  # Convert the tools
+            adapted_tools = tool_adapter.tools() # Get the converted tools
+
+            # Now configure your external agent with the adapted_tools
+            # Example: self.external_agent.set_tools(adapted_tools)
+            print(f"Configuring external agent with adapted tools: {adapted_tools}") # Placeholder
+        else:
+            # Handle no tools case
+            print("No tools provided for MyCustomAgentAdapter.")
+    ```
+
+By creating a `BaseToolAdapter`, you decouple the tool conversion logic from the agent adaptation, making the integration cleaner and more modular. Remember to replace the placeholder examples with the actual conversion logic required by your specific external agent framework.
+
+## BaseConverter
+The `BaseConverterAdapter` plays a crucial role when a CrewAI `Task` requires an agent to return its final output in a specific structured format, such as JSON or a Pydantic model. It bridges the gap between CrewAI's structured output requirements and the capabilities of your external agent.
+
+Its primary responsibilities are:
+1.  **Configuring the Agent for Structured Output:** Based on the `Task`'s requirements (`output_json` or `output_pydantic`), it instructs the associated `BaseAgentAdapter` (and indirectly, the external agent) on what format is expected.
+2.  **Enhancing the System Prompt:** It modifies the agent's system prompt to include clear instructions on *how* to generate the output in the required structure.
+3.  **Post-processing the Result:** It takes the raw output from the agent and attempts to parse, validate, and format it according to the required structure, ultimately returning a string representation (e.g., a JSON string).
+
+Here's how you implement your custom converter adapter:
+
+1.  **Inherit from `BaseConverterAdapter`**:
+    ```python
+    from crewai.agents.agent_adapters.base_converter_adapter import BaseConverterAdapter
+    # Assuming you have your MyCustomAgentAdapter defined
+    # from .my_custom_agent_adapter import MyCustomAgentAdapter
+    from crewai.task import Task
+    from typing import Any
+
+    class MyCustomConverterAdapter(BaseConverterAdapter):
+        # Store the expected output type (e.g., 'json', 'pydantic', 'text')
+        _output_type: str = 'text' 
+        _output_schema: Any = None # Store JSON schema or Pydantic model
+
+        # ... implementation details ...
+    ```
+
+2.  **Implement `__init__`**:
+    The constructor must accept the corresponding `agent_adapter` instance it will work with.
+
+    ```python
+    def __init__(self, agent_adapter: Any): # Use your specific AgentAdapter type hint
+        self.agent_adapter = agent_adapter
+        print(f"Initializing MyCustomConverterAdapter for agent adapter: {type(agent_adapter).__name__}")
+    ```
+
+3.  **Implement `configure_structured_output`**:
+    This method receives the CrewAI `Task` object. You need to check the task's `output_json` and `output_pydantic` attributes to determine the required output structure. Store this information (e.g., in `_output_type` and `_output_schema`) and potentially call configuration methods on your `self.agent_adapter` if the external agent needs specific setup for structured output (which might have been partially handled in the agent adapter's `configure_structured_output` already).
+
+    ```python
+    def configure_structured_output(self, task: Task) -> None:
+        """Configure the expected structured output based on the task."""
+        if task.output_pydantic:
+            self._output_type = 'pydantic'
+            self._output_schema = task.output_pydantic
+            print(f"Converter: Configured for Pydantic output: {self._output_schema.__name__}")
+        elif task.output_json:
+            self._output_type = 'json'
+            self._output_schema = task.output_json
+            print(f"Converter: Configured for JSON output with schema: {self._output_schema}")
+        else:
+            self._output_type = 'text'
+            self._output_schema = None
+            print("Converter: Configured for standard text output.")
+
+        # Optionally, inform the agent adapter if needed
+        # self.agent_adapter.set_output_mode(self._output_type, self._output_schema)
+    ```
+
+4.  **Implement `enhance_system_prompt`**:
+    This method takes the agent's base system prompt string and should append instructions tailored to the currently configured `_output_type` and `_output_schema`. The goal is to guide the LLM powering the agent to produce output in the correct format.
+
+    ```python
+    def enhance_system_prompt(self, base_prompt: str) -> str:
+        """Enhance the system prompt with structured output instructions."""
+        if self._output_type == 'text':
+            return base_prompt # No enhancement needed for plain text
+
+        instructions = "\n\nYour final answer MUST be formatted as "
+        if self._output_type == 'json':
+            schema_str = json.dumps(self._output_schema, indent=2)
+            instructions += f"a JSON object conforming to the following schema:\n```json\n{schema_str}\n```"
+        elif self._output_type == 'pydantic':
+            schema_str = json.dumps(self._output_schema.model_json_schema(), indent=2)
+            instructions += f"a JSON object conforming to the Pydantic model '{self._output_schema.__name__}' with the following schema:\n```json\n{schema_str}\n```"
+
+        instructions += "\nEnsure your entire response is ONLY the valid JSON object, without any introductory text, explanations, or concluding remarks."
+        
+        print(f"Converter: Enhancing prompt for {self._output_type} output.")
+        return base_prompt + instructions
+    ```
+    *Note: The exact prompt engineering might need tuning based on the agent/LLM being used.* 
+
+5.  **Implement `post_process_result`**:
+    This method receives the raw string output from the agent. If structured output was requested (`json` or `pydantic`), you should attempt to parse the string into the expected format. Handle potential parsing errors (e.g., log them, attempt simple fixes, or raise an exception). Crucially, the method must **always return a string**, even if the intermediate format was a dictionary or Pydantic object (e.g., by serializing it back to a JSON string).
+
+    ```python
+    import json
+    from pydantic import ValidationError
+
+    def post_process_result(self, result: str) -> str:
+        """Post-process the agent's result to ensure it matches the expected format."""
+        print(f"Converter: Post-processing result for {self._output_type} output.")
+        if self._output_type == 'json':
+            try:
+                # Attempt to parse and re-serialize to ensure validity and consistent format
+                parsed_json = json.loads(result)
+                # Optional: Validate against self._output_schema if it's a JSON schema dictionary
+                # from jsonschema import validate
+                # validate(instance=parsed_json, schema=self._output_schema)
+                return json.dumps(parsed_json)
+            except json.JSONDecodeError as e:
+                print(f"Error: Failed to parse JSON output: {e}\nRaw output:\n{result}")
+                # Handle error: return raw, raise exception, or try to fix
+                return result # Example: return raw output on failure
+            # except Exception as e: # Catch validation errors if using jsonschema
+            #     print(f"Error: JSON output failed schema validation: {e}\nRaw output:\n{result}")
+            #     return result
+        elif self._output_type == 'pydantic':
+            try:
+                # Attempt to parse into the Pydantic model
+                model_instance = self._output_schema.model_validate_json(result)
+                # Return the model serialized back to JSON
+                return model_instance.model_dump_json()
+            except ValidationError as e:
+                print(f"Error: Failed to validate Pydantic output: {e}\nRaw output:\n{result}")
+                # Handle error
+                return result # Example: return raw output on failure
+            except json.JSONDecodeError as e:
+                 print(f"Error: Failed to parse JSON for Pydantic model: {e}\nRaw output:\n{result}")
+                 return result
+        else: # 'text'
+            return result # No processing needed for plain text
+    ```
+
+By implementing these methods, your `MyCustomConverterAdapter` ensures that structured output requests from CrewAI tasks are correctly handled by your integrated external agent, improving the reliability and usability of your custom agent within the CrewAI framework.
+
+## Out of the Box Adapters
+
+We provide out of the box adapters for the following frameworks:
+1. LangGraph
+2. OpenAI Agents
+
+## Kicking off a crew with adapted agents:
+
+```python
+import json
+import os
+from typing import List
+
+from crewai_tools import SerperDevTool
+from src.crewai import Agent, Crew, Task
+from langchain_openai import ChatOpenAI
+from pydantic import BaseModel
+
+from crewai.agents.agent_adapters.langgraph.langgraph_adapter import (
+    LangGraphAgentAdapter,
+)
+from crewai.agents.agent_adapters.openai_agents.openai_adapter import OpenAIAgentAdapter
+
+# CrewAI Agent
+code_helper_agent = Agent(
+    role="Code Helper",
+    goal="Help users solve coding problems effectively and provide clear explanations.",
+    backstory="You are an experienced programmer with deep knowledge across multiple programming languages and frameworks. You specialize in solving complex coding challenges and explaining solutions clearly.",
+    allow_delegation=False,
+    verbose=True,
+)
+# OpenAI Agent Adapter
+link_finder_agent = OpenAIAgentAdapter(
+    role="Link Finder",
+    goal="Find the most relevant and high-quality resources for coding tasks.",
+    backstory="You are a research specialist with a talent for finding the most helpful resources. You're skilled at using search tools to discover documentation, tutorials, and examples that directly address the user's coding needs.",
+    tools=[SerperDevTool()],
+    allow_delegation=False,
+    verbose=True,
+)
+
+# LangGraph Agent Adapter
+reporter_agent = LangGraphAgentAdapter(
+    role="Reporter",
+    goal="Report the results of the tasks.",
+    backstory="You are a reporter who reports the results of the other tasks",
+    llm=ChatOpenAI(model="gpt-4o"),
+    allow_delegation=True,
+    verbose=True,
+)
+
+
+class Code(BaseModel):
+    code: str
+
+
+task = Task(
+    description="Give an answer to the coding question: {task}",
+    expected_output="A thorough answer to the coding question: {task}",
+    agent=code_helper_agent,
+    output_json=Code,
+)
+task2 = Task(
+    description="Find links to resources that can help with coding tasks. Use the serper tool to find resources that can help.",
+    expected_output="A list of links to resources that can help with coding tasks",
+    agent=link_finder_agent,
+)
+
+
+class Report(BaseModel):
+    code: str
+    links: List[str]
+
+
+task3 = Task(
+    description="Report the results of the tasks.",
+    expected_output="A report of the results of the tasks. this is the code produced and then the links to the resources that can help with the coding task.",
+    agent=reporter_agent,
+    output_json=Report,
+)
+# Use in CrewAI
+crew = Crew(
+    agents=[code_helper_agent, link_finder_agent, reporter_agent],
+    tasks=[task, task2, task3],
+    verbose=True,
+)
+
+result = crew.kickoff(
+    inputs={"task": "How do you implement an abstract class in python?"}
+)
+
+# Print raw result first
+print("Raw result:", result)
+
+# Handle result based on its type
+if hasattr(result, "json_dict") and result.json_dict:
+    json_result = result.json_dict
+    print("\nStructured JSON result:")
+    print(f"{json.dumps(json_result, indent=2)}")
+
+    # Access fields safely
+    if isinstance(json_result, dict):
+        if "code" in json_result:
+            print("\nCode:")
+            print(
+                json_result["code"][:200] + "..."
+                if len(json_result["code"]) > 200
+                else json_result["code"]
+            )
+
+        if "links" in json_result:
+            print("\nLinks:")
+            for link in json_result["links"][:5]:  # Print first 5 links
+                print(f"- {link}")
+            if len(json_result["links"]) > 5:
+                print(f"...and {len(json_result['links']) - 5} more links")
+elif hasattr(result, "pydantic") and result.pydantic:
+    print("\nPydantic model result:")
+    print(result.pydantic.model_dump_json(indent=2))
+else:
+    # Fallback to raw output
+    print("\nNo structured result available, using raw output:")
+    print(result.raw[:500] + "..." if len(result.raw) > 500 else result.raw)
+
+```
\ No newline at end of file
diff --git a/src/crewai/agents/agent_adapters/__init__.py b/src/crewai/agents/agent_adapters/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/crewai/agents/agent_adapters/base_agent_adapter.py b/src/crewai/agents/agent_adapters/base_agent_adapter.py
new file mode 100644
index 00000000..6b8a151d
--- /dev/null
+++ b/src/crewai/agents/agent_adapters/base_agent_adapter.py
@@ -0,0 +1,42 @@
+from abc import ABC, abstractmethod
+from typing import Any, Dict, List, Optional
+
+from pydantic import PrivateAttr
+
+from crewai.agent import BaseAgent
+from crewai.tools import BaseTool
+
+
+class BaseAgentAdapter(BaseAgent, ABC):
+    """Base class for all agent adapters in CrewAI.
+
+    This abstract class defines the common interface and functionality that all
+    agent adapters must implement. It extends BaseAgent to maintain compatibility
+    with the CrewAI framework while adding adapter-specific requirements.
+    """
+
+    adapted_structured_output: bool = False
+    _agent_config: Optional[Dict[str, Any]] = PrivateAttr(default=None)
+
+    model_config = {"arbitrary_types_allowed": True}
+
+    def __init__(self, agent_config: Optional[Dict[str, Any]] = None, **kwargs: Any):
+        super().__init__(adapted_agent=True, **kwargs)
+        self._agent_config = agent_config
+
+    @abstractmethod
+    def configure_tools(self, tools: Optional[List[BaseTool]] = None) -> None:
+        """Configure and adapt tools for the specific agent implementation.
+
+        Args:
+            tools: Optional list of BaseTool instances to be configured
+        """
+        pass
+
+    def configure_structured_output(self, structured_output: Any) -> None:
+        """Configure the structured output for the specific agent implementation.
+
+        Args:
+            structured_output: The structured output to be configured
+        """
+        pass
diff --git a/src/crewai/agents/agent_adapters/base_converter_adapter.py b/src/crewai/agents/agent_adapters/base_converter_adapter.py
new file mode 100644
index 00000000..557e627f
--- /dev/null
+++ b/src/crewai/agents/agent_adapters/base_converter_adapter.py
@@ -0,0 +1,29 @@
+from abc import ABC, abstractmethod
+
+
+class BaseConverterAdapter(ABC):
+    """Base class for all converter adapters in CrewAI.
+
+    This abstract class defines the common interface and functionality that all
+    converter adapters must implement for converting structured output.
+    """
+
+    def __init__(self, agent_adapter):
+        self.agent_adapter = agent_adapter
+
+    @abstractmethod
+    def configure_structured_output(self, task) -> None:
+        """Configure agents to return structured output.
+        Must support json and pydantic output.
+        """
+        pass
+
+    @abstractmethod
+    def enhance_system_prompt(self, base_prompt: str) -> str:
+        """Enhance the system prompt with structured output instructions."""
+        pass
+
+    @abstractmethod
+    def post_process_result(self, result: str) -> str:
+        """Post-process the result to ensure it matches the expected format: string."""
+        pass
diff --git a/src/crewai/agents/agent_adapters/base_tool_adapter.py b/src/crewai/agents/agent_adapters/base_tool_adapter.py
new file mode 100644
index 00000000..f1ee438a
--- /dev/null
+++ b/src/crewai/agents/agent_adapters/base_tool_adapter.py
@@ -0,0 +1,37 @@
+from abc import ABC, abstractmethod
+from typing import Any, List, Optional
+
+from crewai.tools.base_tool import BaseTool
+
+
+class BaseToolAdapter(ABC):
+    """Base class for all tool adapters in CrewAI.
+
+    This abstract class defines the common interface that all tool adapters
+    must implement. It provides the structure for adapting CrewAI tools to
+    different frameworks and platforms.
+    """
+
+    original_tools: List[BaseTool]
+    converted_tools: List[Any]
+
+    def __init__(self, tools: Optional[List[BaseTool]] = None):
+        self.original_tools = tools or []
+        self.converted_tools = []
+
+    @abstractmethod
+    def configure_tools(self, tools: List[BaseTool]) -> None:
+        """Configure and convert tools for the specific implementation.
+
+        Args:
+            tools: List of BaseTool instances to be configured and converted
+        """
+        pass
+
+    def tools(self) -> List[Any]:
+        """Return all converted tools."""
+        return self.converted_tools
+
+    def sanitize_tool_name(self, tool_name: str) -> str:
+        """Sanitize tool name for API compatibility."""
+        return tool_name.replace(" ", "_")
diff --git a/src/crewai/agents/agent_adapters/langgraph/langgraph_adapter.py b/src/crewai/agents/agent_adapters/langgraph/langgraph_adapter.py
new file mode 100644
index 00000000..ea2e373d
--- /dev/null
+++ b/src/crewai/agents/agent_adapters/langgraph/langgraph_adapter.py
@@ -0,0 +1,226 @@
+from typing import Any, AsyncIterable, Dict, List, Optional
+
+from pydantic import Field, PrivateAttr
+
+from crewai.agents.agent_adapters.base_agent_adapter import BaseAgentAdapter
+from crewai.agents.agent_adapters.langgraph.langgraph_tool_adapter import (
+    LangGraphToolAdapter,
+)
+from crewai.agents.agent_adapters.langgraph.structured_output_converter import (
+    LangGraphConverterAdapter,
+)
+from crewai.agents.agent_builder.base_agent import BaseAgent
+from crewai.tools.agent_tools.agent_tools import AgentTools
+from crewai.tools.base_tool import BaseTool
+from crewai.utilities import Logger
+from crewai.utilities.converter import Converter
+from crewai.utilities.events import crewai_event_bus
+from crewai.utilities.events.agent_events import (
+    AgentExecutionCompletedEvent,
+    AgentExecutionErrorEvent,
+    AgentExecutionStartedEvent,
+)
+
+try:
+    from langchain_core.messages import ToolMessage
+    from langgraph.checkpoint.memory import MemorySaver
+    from langgraph.prebuilt import create_react_agent
+
+    LANGGRAPH_AVAILABLE = True
+except ImportError:
+    LANGGRAPH_AVAILABLE = False
+
+
+class LangGraphAgentAdapter(BaseAgentAdapter):
+    """Adapter for LangGraph agents to work with CrewAI."""
+
+    model_config = {"arbitrary_types_allowed": True}
+
+    _logger: Logger = PrivateAttr(default_factory=lambda: Logger())
+    _tool_adapter: LangGraphToolAdapter = PrivateAttr()
+    _graph: Any = PrivateAttr(default=None)
+    _memory: Any = PrivateAttr(default=None)
+    _max_iterations: int = PrivateAttr(default=10)
+    function_calling_llm: Any = Field(default=None)
+    step_callback: Any = Field(default=None)
+
+    model: str = Field(default="gpt-4o")
+    verbose: bool = Field(default=False)
+
+    def __init__(
+        self,
+        role: str,
+        goal: str,
+        backstory: str,
+        tools: Optional[List[BaseTool]] = None,
+        llm: Any = None,
+        max_iterations: int = 10,
+        agent_config: Optional[Dict[str, Any]] = None,
+        **kwargs,
+    ):
+        """Initialize the LangGraph agent adapter."""
+        if not LANGGRAPH_AVAILABLE:
+            raise ImportError(
+                "LangGraph Agent Dependencies are not installed. Please install it using `uv add langchain-core langgraph`"
+            )
+        super().__init__(
+            role=role,
+            goal=goal,
+            backstory=backstory,
+            tools=tools,
+            llm=llm or self.model,
+            agent_config=agent_config,
+            **kwargs,
+        )
+        self._tool_adapter = LangGraphToolAdapter(tools=tools)
+        self._converter_adapter = LangGraphConverterAdapter(self)
+        self._max_iterations = max_iterations
+        self._setup_graph()
+
+    def _setup_graph(self) -> None:
+        """Set up the LangGraph workflow graph."""
+        try:
+            self._memory = MemorySaver()
+
+            converted_tools: List[Any] = self._tool_adapter.tools()
+            if self._agent_config:
+                self._graph = create_react_agent(
+                    model=self.llm,
+                    tools=converted_tools,
+                    checkpointer=self._memory,
+                    debug=self.verbose,
+                    **self._agent_config,
+                )
+            else:
+                self._graph = create_react_agent(
+                    model=self.llm,
+                    tools=converted_tools or [],
+                    checkpointer=self._memory,
+                    debug=self.verbose,
+                )
+
+        except ImportError as e:
+            self._logger.log(
+                "error", f"Failed to import LangGraph dependencies: {str(e)}"
+            )
+            raise
+        except Exception as e:
+            self._logger.log("error", f"Error setting up LangGraph agent: {str(e)}")
+            raise
+
+    def _build_system_prompt(self) -> str:
+        """Build a system prompt for the LangGraph agent."""
+        base_prompt = f"""
+            You are {self.role}.
+        
+            Your goal is: {self.goal}
+
+            Your backstory: {self.backstory}
+
+            When working on tasks, think step-by-step and use the available tools when necessary.
+        """
+        return self._converter_adapter.enhance_system_prompt(base_prompt)
+
+    def execute_task(
+        self,
+        task: Any,
+        context: Optional[str] = None,
+        tools: Optional[List[BaseTool]] = None,
+    ) -> str:
+        """Execute a task using the LangGraph workflow."""
+        self.create_agent_executor(tools)
+
+        self.configure_structured_output(task)
+
+        try:
+            task_prompt = task.prompt() if hasattr(task, "prompt") else str(task)
+
+            if context:
+                task_prompt = self.i18n.slice("task_with_context").format(
+                    task=task_prompt, context=context
+                )
+
+            crewai_event_bus.emit(
+                self,
+                event=AgentExecutionStartedEvent(
+                    agent=self,
+                    tools=self.tools,
+                    task_prompt=task_prompt,
+                    task=task,
+                ),
+            )
+
+            session_id = f"task_{id(task)}"
+
+            config = {"configurable": {"thread_id": session_id}}
+
+            result = self._graph.invoke(
+                {
+                    "messages": [
+                        ("system", self._build_system_prompt()),
+                        ("user", task_prompt),
+                    ]
+                },
+                config,
+            )
+
+            messages = result.get("messages", [])
+            last_message = messages[-1] if messages else None
+
+            final_answer = ""
+            if isinstance(last_message, dict):
+                final_answer = last_message.get("content", "")
+            elif hasattr(last_message, "content"):
+                final_answer = getattr(last_message, "content", "")
+
+            final_answer = (
+                self._converter_adapter.post_process_result(final_answer)
+                or "Task execution completed but no clear answer was provided."
+            )
+            crewai_event_bus.emit(
+                self,
+                event=AgentExecutionCompletedEvent(
+                    agent=self, task=task, output=final_answer
+                ),
+            )
+
+            return final_answer
+
+        except Exception as e:
+            self._logger.log("error", f"Error executing LangGraph task: {str(e)}")
+            crewai_event_bus.emit(
+                self,
+                event=AgentExecutionErrorEvent(
+                    agent=self,
+                    task=task,
+                    error=str(e),
+                ),
+            )
+            raise
+
+    def create_agent_executor(self, tools: Optional[List[BaseTool]] = None) -> None:
+        """Configure the LangGraph agent for execution."""
+        self.configure_tools(tools)
+
+    def configure_tools(self, tools: Optional[List[BaseTool]] = None) -> None:
+        """Configure tools for the LangGraph agent."""
+        if tools:
+            all_tools = list(self.tools or []) + list(tools or [])
+            self._tool_adapter.configure_tools(all_tools)
+            available_tools = self._tool_adapter.tools()
+            self._graph.tools = available_tools
+
+    def get_delegation_tools(self, agents: List[BaseAgent]) -> List[BaseTool]:
+        """Implement delegation tools support for LangGraph."""
+        agent_tools = AgentTools(agents=agents)
+        return agent_tools.tools()
+
+    def get_output_converter(
+        self, llm: Any, text: str, model: Any, instructions: str
+    ) -> Any:
+        """Convert output format if needed."""
+        return Converter(llm=llm, text=text, model=model, instructions=instructions)
+
+    def configure_structured_output(self, task) -> None:
+        """Configure the structured output for LangGraph."""
+        self._converter_adapter.configure_structured_output(task)
diff --git a/src/crewai/agents/agent_adapters/langgraph/langgraph_tool_adapter.py b/src/crewai/agents/agent_adapters/langgraph/langgraph_tool_adapter.py
new file mode 100644
index 00000000..0bc31d20
--- /dev/null
+++ b/src/crewai/agents/agent_adapters/langgraph/langgraph_tool_adapter.py
@@ -0,0 +1,61 @@
+import inspect
+from typing import Any, List, Optional
+
+from crewai.agents.agent_adapters.base_tool_adapter import BaseToolAdapter
+from crewai.tools.base_tool import BaseTool
+
+
+class LangGraphToolAdapter(BaseToolAdapter):
+    """Adapts CrewAI tools to LangGraph agent tool compatible format"""
+
+    def __init__(self, tools: Optional[List[BaseTool]] = None):
+        self.original_tools = tools or []
+        self.converted_tools = []
+
+    def configure_tools(self, tools: List[BaseTool]) -> None:
+        """
+        Configure and convert CrewAI tools to LangGraph-compatible format.
+        LangGraph expects tools in langchain_core.tools format.
+        """
+        from langchain_core.tools import BaseTool, StructuredTool
+
+        converted_tools = []
+        if self.original_tools:
+            all_tools = tools + self.original_tools
+        else:
+            all_tools = tools
+        for tool in all_tools:
+            if isinstance(tool, BaseTool):
+                converted_tools.append(tool)
+                continue
+
+            sanitized_name = self.sanitize_tool_name(tool.name)
+
+            async def tool_wrapper(*args, tool=tool, **kwargs):
+                output = None
+                if len(args) > 0 and isinstance(args[0], str):
+                    output = tool.run(args[0])
+                elif "input" in kwargs:
+                    output = tool.run(kwargs["input"])
+                else:
+                    output = tool.run(**kwargs)
+
+                if inspect.isawaitable(output):
+                    result = await output
+                else:
+                    result = output
+                return result
+
+            converted_tool = StructuredTool(
+                name=sanitized_name,
+                description=tool.description,
+                func=tool_wrapper,
+                args_schema=tool.args_schema,
+            )
+
+            converted_tools.append(converted_tool)
+
+        self.converted_tools = converted_tools
+
+    def tools(self) -> List[Any]:
+        return self.converted_tools or []
diff --git a/src/crewai/agents/agent_adapters/langgraph/structured_output_converter.py b/src/crewai/agents/agent_adapters/langgraph/structured_output_converter.py
new file mode 100644
index 00000000..79f6dcb1
--- /dev/null
+++ b/src/crewai/agents/agent_adapters/langgraph/structured_output_converter.py
@@ -0,0 +1,80 @@
+import json
+
+from crewai.agents.agent_adapters.base_converter_adapter import BaseConverterAdapter
+from crewai.utilities.converter import generate_model_description
+
+
+class LangGraphConverterAdapter(BaseConverterAdapter):
+    """Adapter for handling structured output conversion in LangGraph agents"""
+
+    def __init__(self, agent_adapter):
+        """Initialize the converter adapter with a reference to the agent adapter"""
+        self.agent_adapter = agent_adapter
+        self._output_format = None
+        self._schema = None
+        self._system_prompt_appendix = None
+
+    def configure_structured_output(self, task) -> None:
+        """Configure the structured output for LangGraph."""
+        if not (task.output_json or task.output_pydantic):
+            self._output_format = None
+            self._schema = None
+            self._system_prompt_appendix = None
+            return
+
+        if task.output_json:
+            self._output_format = "json"
+            self._schema = generate_model_description(task.output_json)
+        elif task.output_pydantic:
+            self._output_format = "pydantic"
+            self._schema = generate_model_description(task.output_pydantic)
+
+        self._system_prompt_appendix = self._generate_system_prompt_appendix()
+
+    def _generate_system_prompt_appendix(self) -> str:
+        """Generate an appendix for the system prompt to enforce structured output"""
+        if not self._output_format or not self._schema:
+            return ""
+
+        return f"""
+Important: Your final answer MUST be provided in the following structured format:
+
+{self._schema}
+
+DO NOT include any markdown code blocks, backticks, or other formatting around your response. 
+The output should be raw JSON that exactly matches the specified schema.
+"""
+
+    def enhance_system_prompt(self, original_prompt: str) -> str:
+        """Add structured output instructions to the system prompt if needed"""
+        if not self._system_prompt_appendix:
+            return original_prompt
+
+        return f"{original_prompt}\n{self._system_prompt_appendix}"
+
+    def post_process_result(self, result: str) -> str:
+        """Post-process the result to ensure it matches the expected format"""
+        if not self._output_format:
+            return result
+
+        # Try to extract valid JSON if it's wrapped in code blocks or other text
+        if self._output_format in ["json", "pydantic"]:
+            try:
+                # First, try to parse as is
+                json.loads(result)
+                return result
+            except json.JSONDecodeError:
+                # Try to extract JSON from the text
+                import re
+
+                json_match = re.search(r"(\{.*\})", result, re.DOTALL)
+                if json_match:
+                    try:
+                        extracted = json_match.group(1)
+                        # Validate it's proper JSON
+                        json.loads(extracted)
+                        return extracted
+                    except:
+                        pass
+
+        return result
diff --git a/src/crewai/agents/agent_adapters/openai_agents/openai_adapter.py b/src/crewai/agents/agent_adapters/openai_agents/openai_adapter.py
new file mode 100644
index 00000000..ac368c1a
--- /dev/null
+++ b/src/crewai/agents/agent_adapters/openai_agents/openai_adapter.py
@@ -0,0 +1,178 @@
+from typing import Any, List, Optional
+
+from pydantic import Field, PrivateAttr
+
+from crewai.agents.agent_adapters.base_agent_adapter import BaseAgentAdapter
+from crewai.agents.agent_adapters.openai_agents.structured_output_converter import (
+    OpenAIConverterAdapter,
+)
+from crewai.agents.agent_builder.base_agent import BaseAgent
+from crewai.tools import BaseTool
+from crewai.tools.agent_tools.agent_tools import AgentTools
+from crewai.utilities import Logger
+from crewai.utilities.events import crewai_event_bus
+from crewai.utilities.events.agent_events import (
+    AgentExecutionCompletedEvent,
+    AgentExecutionErrorEvent,
+    AgentExecutionStartedEvent,
+)
+
+try:
+    from agents import Agent as OpenAIAgent  # type: ignore
+    from agents import Runner, enable_verbose_stdout_logging  # type: ignore
+
+    from .openai_agent_tool_adapter import OpenAIAgentToolAdapter
+
+    OPENAI_AVAILABLE = True
+except ImportError:
+    OPENAI_AVAILABLE = False
+
+
+class OpenAIAgentAdapter(BaseAgentAdapter):
+    """Adapter for OpenAI Assistants"""
+
+    model_config = {"arbitrary_types_allowed": True}
+
+    _openai_agent: "OpenAIAgent" = PrivateAttr()
+    _logger: Logger = PrivateAttr(default_factory=lambda: Logger())
+    _active_thread: Optional[str] = PrivateAttr(default=None)
+    function_calling_llm: Any = Field(default=None)
+    step_callback: Any = Field(default=None)
+    _tool_adapter: "OpenAIAgentToolAdapter" = PrivateAttr()
+    _converter_adapter: OpenAIConverterAdapter = PrivateAttr()
+
+    def __init__(
+        self,
+        model: str = "gpt-4o-mini",
+        tools: Optional[List[BaseTool]] = None,
+        agent_config: Optional[dict] = None,
+        **kwargs,
+    ):
+        if not OPENAI_AVAILABLE:
+            raise ImportError(
+                "OpenAI Agent Dependencies are not installed. Please install it using `uv add openai-agents`"
+            )
+        else:
+            role = kwargs.pop("role", None)
+            goal = kwargs.pop("goal", None)
+            backstory = kwargs.pop("backstory", None)
+            super().__init__(
+                role=role,
+                goal=goal,
+                backstory=backstory,
+                tools=tools,
+                agent_config=agent_config,
+                **kwargs,
+            )
+            self._tool_adapter = OpenAIAgentToolAdapter(tools=tools)
+            self.llm = model
+            self._converter_adapter = OpenAIConverterAdapter(self)
+
+    def _build_system_prompt(self) -> str:
+        """Build a system prompt for the OpenAI agent."""
+        base_prompt = f"""
+            You are {self.role}.
+        
+            Your goal is: {self.goal}
+
+            Your backstory: {self.backstory}
+
+            When working on tasks, think step-by-step and use the available tools when necessary.
+        """
+        return self._converter_adapter.enhance_system_prompt(base_prompt)
+
+    def execute_task(
+        self,
+        task: Any,
+        context: Optional[str] = None,
+        tools: Optional[List[BaseTool]] = None,
+    ) -> str:
+        """Execute a task using the OpenAI Assistant"""
+        self._converter_adapter.configure_structured_output(task)
+        self.create_agent_executor(tools)
+
+        if self.verbose:
+            enable_verbose_stdout_logging()
+
+        try:
+            task_prompt = task.prompt()
+            if context:
+                task_prompt = self.i18n.slice("task_with_context").format(
+                    task=task_prompt, context=context
+                )
+            crewai_event_bus.emit(
+                self,
+                event=AgentExecutionStartedEvent(
+                    agent=self,
+                    tools=self.tools,
+                    task_prompt=task_prompt,
+                    task=task,
+                ),
+            )
+            result = self.agent_executor.run_sync(self._openai_agent, task_prompt)
+            final_answer = self.handle_execution_result(result)
+            crewai_event_bus.emit(
+                self,
+                event=AgentExecutionCompletedEvent(
+                    agent=self, task=task, output=final_answer
+                ),
+            )
+            return final_answer
+
+        except Exception as e:
+            self._logger.log("error", f"Error executing OpenAI task: {str(e)}")
+            crewai_event_bus.emit(
+                self,
+                event=AgentExecutionErrorEvent(
+                    agent=self,
+                    task=task,
+                    error=str(e),
+                ),
+            )
+            raise
+
+    def create_agent_executor(self, tools: Optional[List[BaseTool]] = None) -> None:
+        """
+        Configure the OpenAI agent for execution.
+        While OpenAI handles execution differently through Runner,
+        we can use this method to set up tools and configurations.
+        """
+        all_tools = list(self.tools or []) + list(tools or [])
+
+        instructions = self._build_system_prompt()
+        self._openai_agent = OpenAIAgent(
+            name=self.role,
+            instructions=instructions,
+            model=self.llm,
+            **self._agent_config or {},
+        )
+
+        if all_tools:
+            self.configure_tools(all_tools)
+
+        self.agent_executor = Runner
+
+    def configure_tools(self, tools: Optional[List[BaseTool]] = None) -> None:
+        """Configure tools for the OpenAI Assistant"""
+        if tools:
+            self._tool_adapter.configure_tools(tools)
+            if self._tool_adapter.converted_tools:
+                self._openai_agent.tools = self._tool_adapter.converted_tools
+
+    def handle_execution_result(self, result: Any) -> str:
+        """Process OpenAI Assistant execution result converting any structured output to a string"""
+        return self._converter_adapter.post_process_result(result.final_output)
+
+    def get_delegation_tools(self, agents: List[BaseAgent]) -> List[BaseTool]:
+        """Implement delegation tools support"""
+        agent_tools = AgentTools(agents=agents)
+        tools = agent_tools.tools()
+        return tools
+
+    def configure_structured_output(self, task) -> None:
+        """Configure the structured output for the specific agent implementation.
+
+        Args:
+            structured_output: The structured output to be configured
+        """
+        self._converter_adapter.configure_structured_output(task)
diff --git a/src/crewai/agents/agent_adapters/openai_agents/openai_agent_tool_adapter.py b/src/crewai/agents/agent_adapters/openai_agents/openai_agent_tool_adapter.py
new file mode 100644
index 00000000..92eeb7b0
--- /dev/null
+++ b/src/crewai/agents/agent_adapters/openai_agents/openai_agent_tool_adapter.py
@@ -0,0 +1,91 @@
+import inspect
+from typing import Any, List, Optional
+
+from agents import FunctionTool, Tool
+
+from crewai.agents.agent_adapters.base_tool_adapter import BaseToolAdapter
+from crewai.tools import BaseTool
+
+
+class OpenAIAgentToolAdapter(BaseToolAdapter):
+    """Adapter for OpenAI Assistant tools"""
+
+    def __init__(self, tools: Optional[List[BaseTool]] = None):
+        self.original_tools = tools or []
+
+    def configure_tools(self, tools: List[BaseTool]) -> None:
+        """Configure tools for the OpenAI Assistant"""
+        if self.original_tools:
+            all_tools = tools + self.original_tools
+        else:
+            all_tools = tools
+        if all_tools:
+            self.converted_tools = self._convert_tools_to_openai_format(all_tools)
+
+    def _convert_tools_to_openai_format(
+        self, tools: Optional[List[BaseTool]]
+    ) -> List[Tool]:
+        """Convert CrewAI tools to OpenAI Assistant tool format"""
+        if not tools:
+            return []
+
+        def sanitize_tool_name(name: str) -> str:
+            """Convert tool name to match OpenAI's required pattern"""
+            import re
+
+            sanitized = re.sub(r"[^a-zA-Z0-9_-]", "_", name).lower()
+            return sanitized
+
+        def create_tool_wrapper(tool: BaseTool):
+            """Create a wrapper function that handles the OpenAI function tool interface"""
+
+            async def wrapper(context_wrapper: Any, arguments: Any) -> Any:
+                # Get the parameter name from the schema
+                param_name = list(
+                    tool.args_schema.model_json_schema()["properties"].keys()
+                )[0]
+
+                # Handle different argument types
+                if isinstance(arguments, dict):
+                    args_dict = arguments
+                elif isinstance(arguments, str):
+                    try:
+                        import json
+
+                        args_dict = json.loads(arguments)
+                    except json.JSONDecodeError:
+                        args_dict = {param_name: arguments}
+                else:
+                    args_dict = {param_name: str(arguments)}
+
+                # Run the tool with the processed arguments
+                output = tool._run(**args_dict)
+
+                # Await if the tool returned a coroutine
+                if inspect.isawaitable(output):
+                    result = await output
+                else:
+                    result = output
+
+                # Ensure the result is JSON serializable
+                if isinstance(result, (dict, list, str, int, float, bool, type(None))):
+                    return result
+                return str(result)
+
+            return wrapper
+
+        openai_tools = []
+        for tool in tools:
+            schema = tool.args_schema.model_json_schema()
+
+            schema.update({"additionalProperties": False, "type": "object"})
+
+            openai_tool = FunctionTool(
+                name=sanitize_tool_name(tool.name),
+                description=tool.description,
+                params_json_schema=schema,
+                on_invoke_tool=create_tool_wrapper(tool),
+            )
+            openai_tools.append(openai_tool)
+
+        return openai_tools
diff --git a/src/crewai/agents/agent_adapters/openai_agents/structured_output_converter.py b/src/crewai/agents/agent_adapters/openai_agents/structured_output_converter.py
new file mode 100644
index 00000000..252374bf
--- /dev/null
+++ b/src/crewai/agents/agent_adapters/openai_agents/structured_output_converter.py
@@ -0,0 +1,122 @@
+import json
+import re
+
+from crewai.agents.agent_adapters.base_converter_adapter import BaseConverterAdapter
+from crewai.utilities.converter import generate_model_description
+from crewai.utilities.i18n import I18N
+
+
+class OpenAIConverterAdapter(BaseConverterAdapter):
+    """
+    Adapter for handling structured output conversion in OpenAI agents.
+
+    This adapter enhances the OpenAI agent to handle structured output formats
+    and post-processes the results when needed.
+
+    Attributes:
+        _output_format: The expected output format (json, pydantic, or None)
+        _schema: The schema description for the expected output
+        _output_model: The Pydantic model for the output
+    """
+
+    def __init__(self, agent_adapter):
+        """Initialize the converter adapter with a reference to the agent adapter"""
+        self.agent_adapter = agent_adapter
+        self._output_format = None
+        self._schema = None
+        self._output_model = None
+
+    def configure_structured_output(self, task) -> None:
+        """
+        Configure the structured output for OpenAI agent based on task requirements.
+
+        Args:
+            task: The task containing output format requirements
+        """
+        # Reset configuration
+        self._output_format = None
+        self._schema = None
+        self._output_model = None
+
+        # If no structured output is required, return early
+        if not (task.output_json or task.output_pydantic):
+            return
+
+        # Configure based on task output format
+        if task.output_json:
+            self._output_format = "json"
+            self._schema = generate_model_description(task.output_json)
+            self.agent_adapter._openai_agent.output_type = task.output_json
+            self._output_model = task.output_json
+        elif task.output_pydantic:
+            self._output_format = "pydantic"
+            self._schema = generate_model_description(task.output_pydantic)
+            self.agent_adapter._openai_agent.output_type = task.output_pydantic
+            self._output_model = task.output_pydantic
+
+    def enhance_system_prompt(self, base_prompt: str) -> str:
+        """
+        Enhance the base system prompt with structured output requirements if needed.
+
+        Args:
+            base_prompt: The original system prompt
+
+        Returns:
+            Enhanced system prompt with output format instructions if needed
+        """
+        if not self._output_format:
+            return base_prompt
+
+        output_schema = (
+            I18N()
+            .slice("formatted_task_instructions")
+            .format(output_format=self._schema)
+        )
+
+        return f"{base_prompt}\n\n{output_schema}"
+
+    def post_process_result(self, result: str) -> str:
+        """
+        Post-process the result to ensure it matches the expected format.
+
+        This method attempts to extract valid JSON from the result if necessary.
+
+        Args:
+            result: The raw result from the agent
+
+        Returns:
+            Processed result conforming to the expected output format
+        """
+        if not self._output_format:
+            return result
+        # Try to extract valid JSON if it's wrapped in code blocks or other text
+        if isinstance(result, str) and self._output_format in ["json", "pydantic"]:
+            # First, try to parse as is
+            try:
+                json.loads(result)
+                return result
+            except json.JSONDecodeError:
+                # Try to extract JSON from markdown code blocks
+                code_block_pattern = r"```(?:json)?\s*([\s\S]*?)```"
+                code_blocks = re.findall(code_block_pattern, result)
+
+                for block in code_blocks:
+                    try:
+                        json.loads(block.strip())
+                        return block.strip()
+                    except json.JSONDecodeError:
+                        continue
+
+                # Try to extract any JSON-like structure
+                json_pattern = r"(\{[\s\S]*\})"
+                json_matches = re.findall(json_pattern, result, re.DOTALL)
+
+                for match in json_matches:
+                    try:
+                        json.loads(match)
+                        return match
+                    except json.JSONDecodeError:
+                        continue
+
+        # If all extraction attempts fail, return the original
+        return str(result)
diff --git a/src/crewai/agents/agent_builder/base_agent.py b/src/crewai/agents/agent_builder/base_agent.py
index f11c74ae..a82cd12d 100644
--- a/src/crewai/agents/agent_builder/base_agent.py
+++ b/src/crewai/agents/agent_builder/base_agent.py
@@ -62,8 +62,6 @@ class BaseAgent(ABC, BaseModel):
             Abstract method to execute a task.
         create_agent_executor(tools=None) -> None:
             Abstract method to create an agent executor.
-        _parse_tools(tools: List[BaseTool]) -> List[Any]:
-            Abstract method to parse tools.
         get_delegation_tools(agents: List["BaseAgent"]):
             Abstract method to set the agents task tools for handling delegation and question asking to other agents in crew.
         get_output_converter(llm, model, instructions):
@@ -154,6 +152,9 @@ class BaseAgent(ABC, BaseModel):
     callbacks: List[Callable] = Field(
         default=[], description="Callbacks to be used for the agent"
     )
+    adapted_agent: bool = Field(
+        default=False, description="Whether the agent is adapted"
+    )
 
     @model_validator(mode="before")
     @classmethod
@@ -170,15 +171,15 @@ class BaseAgent(ABC, BaseModel):
         tool meets these criteria, it is processed and added to the list of
         tools. Otherwise, a ValueError is raised.
         """
+        if not tools:
+            return []
+
         processed_tools = []
+        required_attrs = ["name", "func", "description"]
         for tool in tools:
             if isinstance(tool, BaseTool):
                 processed_tools.append(tool)
-            elif (
-                hasattr(tool, "name")
-                and hasattr(tool, "func")
-                and hasattr(tool, "description")
-            ):
+            elif all(hasattr(tool, attr) for attr in required_attrs):
                 # Tool has the required attributes, create a Tool instance
                 processed_tools.append(Tool.from_langchain(tool))
             else:
@@ -260,13 +261,6 @@ class BaseAgent(ABC, BaseModel):
         """Set the task tools that init BaseAgenTools class."""
         pass
 
-    @abstractmethod
-    def get_output_converter(
-        self, llm: Any, text: str, model: type[BaseModel] | None, instructions: str
-    ) -> Converter:
-        """Get the converter class for the agent to create json/pydantic outputs."""
-        pass
-
     def copy(self: T) -> T:  # type: ignore # Signature of "copy" incompatible with supertype "BaseModel"
         """Create a deep copy of the Agent."""
         exclude = {
diff --git a/src/crewai/utilities/converter.py b/src/crewai/utilities/converter.py
index b16677ac..a6144868 100644
--- a/src/crewai/utilities/converter.py
+++ b/src/crewai/utilities/converter.py
@@ -216,7 +216,7 @@ def convert_with_instructions(
 
 def get_conversion_instructions(model: Type[BaseModel], llm: Any) -> str:
     instructions = "Please convert the following text into valid JSON."
-    if llm.supports_function_calling():
+    if llm and not isinstance(llm, str) and llm.supports_function_calling():
         model_schema = PydanticSchemaParser(model=model).get_schema()
         instructions += (
             f"\n\nOutput ONLY the valid JSON and nothing else.\n\n"


Test patch:
diff --git a/tests/agents/agent_adapters/test_base_agent_adapter.py b/tests/agents/agent_adapters/test_base_agent_adapter.py
new file mode 100644
index 00000000..2da90b71
--- /dev/null
+++ b/tests/agents/agent_adapters/test_base_agent_adapter.py
@@ -0,0 +1,113 @@
+from typing import Any, Dict, List, Optional
+
+import pytest
+from pydantic import BaseModel
+
+from crewai.agent import BaseAgent
+from crewai.agents.agent_adapters.base_agent_adapter import BaseAgentAdapter
+from crewai.tools import BaseTool
+from crewai.utilities.token_counter_callback import TokenProcess
+
+
+# Concrete implementation for testing
+class ConcreteAgentAdapter(BaseAgentAdapter):
+    def configure_tools(
+        self, tools: Optional[List[BaseTool]] = None, **kwargs: Any
+    ) -> None:
+        # Simple implementation for testing
+        self.tools = tools or []
+
+    def execute_task(
+        self,
+        task: Any,
+        context: Optional[str] = None,
+        tools: Optional[List[Any]] = None,
+    ) -> str:
+        # Dummy implementation needed due to BaseAgent inheritance
+        return "Task executed"
+
+    def create_agent_executor(self, tools: Optional[List[BaseTool]] = None) -> Any:
+        # Dummy implementation
+        return None
+
+    def get_delegation_tools(
+        self, tools: List[BaseTool], tool_map: Optional[Dict[str, BaseTool]]
+    ) -> List[BaseTool]:
+        # Dummy implementation
+        return []
+
+    def _parse_output(self, agent_output: Any, token_process: TokenProcess):
+        # Dummy implementation
+        pass
+
+    def get_output_converter(self, tools: Optional[List[BaseTool]] = None) -> Any:
+        # Dummy implementation
+        return None
+
+
+def test_base_agent_adapter_initialization():
+    """Test initialization of the concrete agent adapter."""
+    adapter = ConcreteAgentAdapter(
+        role="test role", goal="test goal", backstory="test backstory"
+    )
+    assert isinstance(adapter, BaseAgent)
+    assert isinstance(adapter, BaseAgentAdapter)
+    assert adapter.role == "test role"
+    assert adapter._agent_config is None
+    assert adapter.adapted_structured_output is False
+
+
+def test_base_agent_adapter_initialization_with_config():
+    """Test initialization with agent_config."""
+    config = {"model": "gpt-4"}
+    adapter = ConcreteAgentAdapter(
+        agent_config=config,
+        role="test role",
+        goal="test goal",
+        backstory="test backstory",
+    )
+    assert adapter._agent_config == config
+
+
+def test_configure_tools_method_exists():
+    """Test that configure_tools method exists and can be called."""
+    adapter = ConcreteAgentAdapter(
+        role="test role", goal="test goal", backstory="test backstory"
+    )
+    # Create dummy tools if needed, or pass None
+    tools = []
+    adapter.configure_tools(tools)
+    assert hasattr(adapter, "tools")
+    assert adapter.tools == tools
+
+
+def test_configure_structured_output_method_exists():
+    """Test that configure_structured_output method exists and can be called."""
+    adapter = ConcreteAgentAdapter(
+        role="test role", goal="test goal", backstory="test backstory"
+    )
+
+    # Define a dummy structure or pass None/Any
+    class DummyOutput(BaseModel):
+        data: str
+
+    structured_output = DummyOutput
+    adapter.configure_structured_output(structured_output)
+    # Add assertions here if configure_structured_output modifies state
+    # For now, just ensuring it runs without error is sufficient
+    pass
+
+
+def test_base_agent_adapter_inherits_base_agent():
+    """Test that BaseAgentAdapter inherits from BaseAgent."""
+    assert issubclass(BaseAgentAdapter, BaseAgent)
+
+
+class ConcreteAgentAdapterWithoutRequiredMethods(BaseAgentAdapter):
+    pass
+
+
+def test_base_agent_adapter_fails_without_required_methods():
+    """Test that BaseAgentAdapter fails without required methods."""
+    with pytest.raises(TypeError):
+        ConcreteAgentAdapterWithoutRequiredMethods()  # type: ignore
diff --git a/tests/agents/agent_adapters/test_base_tool_adapter.py b/tests/agents/agent_adapters/test_base_tool_adapter.py
new file mode 100644
index 00000000..3003d92c
--- /dev/null
+++ b/tests/agents/agent_adapters/test_base_tool_adapter.py
@@ -0,0 +1,94 @@
+from typing import Any, List
+from unittest.mock import Mock
+
+import pytest
+
+from crewai.agents.agent_adapters.base_tool_adapter import BaseToolAdapter
+from crewai.tools.base_tool import BaseTool
+
+
+class ConcreteToolAdapter(BaseToolAdapter):
+    def configure_tools(self, tools: List[BaseTool]) -> None:
+        self.converted_tools = [f"converted_{tool.name}" for tool in tools]
+
+
+@pytest.fixture
+def mock_tool_1():
+    tool = Mock(spec=BaseTool)
+    tool.name = "Mock Tool 1"
+    return tool
+
+
+@pytest.fixture
+def mock_tool_2():
+    tool = Mock(spec=BaseTool)
+    tool.name = "MockTool2"
+    return tool
+
+
+@pytest.fixture
+def tools_list(mock_tool_1, mock_tool_2):
+    return [mock_tool_1, mock_tool_2]
+
+
+def test_initialization_with_tools(tools_list):
+    adapter = ConcreteToolAdapter(tools=tools_list)
+    assert adapter.original_tools == tools_list
+    assert adapter.converted_tools == []  # Conversion happens in configure_tools
+
+
+def test_initialization_without_tools():
+    adapter = ConcreteToolAdapter()
+    assert adapter.original_tools == []
+    assert adapter.converted_tools == []
+
+
+def test_configure_tools(tools_list):
+    adapter = ConcreteToolAdapter()
+    adapter.configure_tools(tools_list)
+    assert adapter.converted_tools == ["converted_Mock Tool 1", "converted_MockTool2"]
+    assert adapter.original_tools == []  # original_tools is only set in init
+
+    adapter_with_init_tools = ConcreteToolAdapter(tools=tools_list)
+    adapter_with_init_tools.configure_tools(tools_list)
+    assert adapter_with_init_tools.converted_tools == [
+        "converted_Mock Tool 1",
+        "converted_MockTool2",
+    ]
+    assert adapter_with_init_tools.original_tools == tools_list
+
+
+def test_tools_method(tools_list):
+    adapter = ConcreteToolAdapter()
+    adapter.configure_tools(tools_list)
+    assert adapter.tools() == ["converted_Mock Tool 1", "converted_MockTool2"]
+
+
+def test_tools_method_empty():
+    adapter = ConcreteToolAdapter()
+    assert adapter.tools() == []
+
+
+def test_sanitize_tool_name_with_spaces():
+    adapter = ConcreteToolAdapter()
+    assert adapter.sanitize_tool_name("Tool With Spaces") == "Tool_With_Spaces"
+
+
+def test_sanitize_tool_name_without_spaces():
+    adapter = ConcreteToolAdapter()
+    assert adapter.sanitize_tool_name("ToolWithoutSpaces") == "ToolWithoutSpaces"
+
+
+def test_sanitize_tool_name_empty():
+    adapter = ConcreteToolAdapter()
+    assert adapter.sanitize_tool_name("") == ""
+
+
+class ConcreteToolAdapterWithoutRequiredMethods(BaseToolAdapter):
+    pass
+
+
+def test_tool_adapted_fails_without_required_methods():
+    """Test that BaseToolAdapter fails without required methods."""
+    with pytest.raises(TypeError):
+        ConcreteToolAdapterWithoutRequiredMethods()  # type: ignore
diff --git a/tests/agents/agent_builder/base_agent_test.py b/tests/agents/agent_builder/base_agent_test.py
index 99f66dcc..59faa6ba 100644
--- a/tests/agents/agent_builder/base_agent_test.py
+++ b/tests/agents/agent_builder/base_agent_test.py
@@ -18,9 +18,6 @@ class MockAgent(BaseAgent):
 
     def create_agent_executor(self, tools=None) -> None: ...
 
-    def _parse_tools(self, tools: List[BaseTool]) -> List[BaseTool]:
-        return []
-
     def get_delegation_tools(self, agents: List["BaseAgent"]): ...
 
     def get_output_converter(