Skip to content

feat: Agent as mcp in chatagent #2272

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
408 changes: 408 additions & 0 deletions camel/agents/chat_agent.py

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion camel/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
from .constants import Constants
from .deduplication import DeduplicationResult, deduplicate_internally
from .mcp import MCPServer
from .response_format import get_pydantic_model
from .response_format import get_pydantic_model, model_from_json_schema
from .token_counting import (
AnthropicTokenCounter,
BaseTokenCounter,
Expand Down Expand Up @@ -90,4 +90,5 @@
"BatchProcessor",
"with_timeout",
"MCPServer",
"model_from_json_schema",
]
82 changes: 80 additions & 2 deletions camel/utils/response_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@

import inspect
import json
from typing import Callable, Type, Union
from typing import Any, Callable, Dict, List, Optional, Type, Union

from pydantic import BaseModel, create_model
from pydantic import BaseModel, Field, create_model


def get_pydantic_model(
Expand Down Expand Up @@ -61,3 +61,81 @@ def get_pydantic_model(
if issubclass(input_data, BaseModel):
return input_data
raise ValueError("Invalid input data provided.")


TYPE_MAPPING = {
"integer": int,
"number": float,
"string": str,
"boolean": bool,
"array": list,
"object": dict,
}


def model_from_json_schema(
name: str,
schema: Dict[str, Any],
) -> Type[BaseModel]:
r"""Create a Pydantic model from a JSON schema.

Args:
name (str): The name of the model.
schema (Dict[str, Any]): The JSON schema to create the model from.

Returns:
Type[BaseModel]: The Pydantic model.
"""
properties = schema.get("properties", {})
required_fields = set(schema.get("required", []))
fields: Dict[str, Any] = {}

for field_name, field_schema in properties.items():
json_type = field_schema.get("type", "string")
# Handle nested objects recursively.
if json_type == "object" and "properties" in field_schema:
py_type = model_from_json_schema(
f"{name}_{field_name}", field_schema
)
elif json_type == "array":
# Process array items if available.
items_schema = field_schema.get("items", {"type": "string"})
items_type: Type[Any] = TYPE_MAPPING.get(
items_schema.get("type", "string"), str
)
if (
items_schema.get("type") == "object"
and "properties" in items_schema
):
items_type = model_from_json_schema(
f"{name}_{field_name}_item", items_schema
)
py_type = List[items_type] # type: ignore[assignment, valid-type]
else:
py_type = TYPE_MAPPING.get(json_type, str)

# Handle nullable fields.
if field_schema.get("nullable", False):
py_type = Optional[py_type] # type: ignore[assignment]

# Construct constraints if available.
constraints = {}
if "minLength" in field_schema:
constraints["min_length"] = field_schema["minLength"]
if "maxLength" in field_schema:
constraints["max_length"] = field_schema["maxLength"]
if "minimum" in field_schema:
constraints["ge"] = field_schema["minimum"]
if "maximum" in field_schema:
constraints["le"] = field_schema["maximum"]

default_value = field_schema.get("default", None)
if field_name in required_fields:
fields[field_name] = (py_type, Field(..., **constraints))
else:
fields[field_name] = (
Optional[py_type],
Field(default_value, **constraints),
)

return create_model(name, **fields) # type: ignore[call-overload]
57 changes: 57 additions & 0 deletions examples/agent/mcp/mcp_server_output/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# SearchAgentMCP

This is an MCP server for the search agent.

## Setup

1. Make sure you have the required dependencies:
```
pip install camel-ai mcp-server-fastmcp
```

2. Set the required environment variables:
```
# No API key required
```

3. Run the server:
```
python searchagentmcp.py
```

## Usage

The server exposes the following tools:

- `step(name, message, response_format=None)`: Execute a chat step
- `reset()`: Reset all agents
- `set_output_language(language)`: Set the output language

And the following resources:

- `agent://`: Get information about all agents
- `agent://{name}`: Get information about a specific agent
- `history://{name}`: Get the chat history for a specific agent
- `tool://{name}`: Get available tools for a specific agent

## Example

```python
from mcp.client import Client

client = Client("ws://localhost:8000")
await client.connect()

# Chat with the agent
response = await client.call("step", {
"name": "search",
"message": "Hello, how are you?"
})
print(response)

# Reset the agent
await client.call("reset")

# Disconnect
await client.disconnect()
```
209 changes: 209 additions & 0 deletions examples/agent/mcp/mcp_server_output/searchagentmcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
import logging
from typing import Any, Dict, Optional

from mcp.server.fastmcp import Context, FastMCP

from camel.agents import ChatAgent
from camel.models import ModelFactory
from camel.types import ModelType
from camel.utils import model_from_json_schema

# Prevent logging since MCP needs to use stdout
root_logger = logging.getLogger()
root_logger.handlers = []
root_logger.addHandler(logging.NullHandler())

# Create MCP server
mcp = FastMCP("SearchAgentMCP", dependencies=["camel-ai"])

# ========================================================================
# AGENT CONFIGURATION
# ========================================================================

# Define the model
model = ModelFactory.create(model_type=ModelType.GPT_3_5_TURBO)

# No tools defined for this agent

# Create the agent
search_agent = ChatAgent(
model=model,
system_message="You are a helpful assistant that can search the web.",
)

search_agent_description = """
A helpful assistant that can search the web.
"""

# Provide a list of agents with names
agents_dict = {
"search": search_agent,
}

# Provide descriptions for each agent
description_dict = {
"search": search_agent_description,
}
# ========================================================================


@mcp.tool()
async def step(
name: str,
message: str,
ctx: Context,
response_format: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Execute a single step in the chat session with the agent.

Args:
name: The name of the agent to use
message: The input message for the agent
response_format: Optional schema for structured response

Returns:
A dictionary containing the response from the agent
"""
try:
agent = agents_dict[name]
except KeyError:
return {
"status": "error",
"message": f"Agent with name {name} not found",
}

format_cls = None
if response_format:
format_cls = model_from_json_schema(
"DynamicResponseFormat", response_format
)

ctx.info(f"The agent {name} is processing the message: {message}")
response = await agent.astep(message, format_cls)

return {
"status": "success",
"messages": [msg.to_dict() for msg in response.msgs],
"terminated": response.terminated,
"info": response.info,
}


@mcp.tool()
def reset(ctx: Context) -> Dict[str, str]:
"""Reset the chat agent to its initial state.
Returns:
A dictionary containing the status of the reset operation
"""
for agent in agents_dict.values():
agent.reset()
ctx.info("All agents reset successfully")
return {"status": "success", "message": "All agents reset successfully"}


@mcp.tool()
def set_output_language(language: str, ctx: Context) -> Dict[str, str]:
"""Set the output language for the chat agent.
Args:
language: The language to set the output language to

Returns:
A dictionary containing the status of the language setting operation
"""
for agent in agents_dict.values():
agent.output_language = language
ctx.info(f"Output language set to '{language}'")
return {
"status": "success",
"message": f"Output language set to '{language}'",
}


@mcp.resource("agent://")
def get_agents_info() -> Dict[str, Any]:
"""Get information about all agents provided by the server.
Returns:
A dictionary containing information about all agents
"""
return description_dict


@mcp.resource("history://{name}")
def get_chat_history(name: str) -> Dict[str, Any]:
"""Get the chat history for the given agent.
Args:
name: The name of the agent to get the chat history for

Returns:
A dictionary containing the chat history for the given agent
"""
try:
agent = agents_dict[name]
except KeyError:
return {
"status": "error",
"message": f"Agent with name {name} not found",
}
return agent.chat_history


@mcp.resource("agent://{name}")
def get_agent_info(name: str) -> Dict[str, Any]:
"""Get information about the given agent.
Args:
name: The name of the agent to get information for

Returns:
A dictionary containing information about the given agent
"""
try:
agent = agents_dict[name]
except KeyError:
return {
"status": "error",
"message": f"Agent with name {name} not found",
}
info = {
"agent_id": agent.agent_id,
"model_type": str(agent.model_type),
"token_limit": agent.token_limit,
"output_language": agent.output_language,
"description": description_dict[name],
}
return info


@mcp.resource("tool://{name}")
def get_available_tools(name: str) -> Dict[str, Any]:
"""Get a list of available internal tools.
Args:
name: The name of the agent to get the available tools for

Returns:
A dictionary containing the available internal tools
"""
try:
agent = agents_dict[name]
except KeyError:
return {
"status": "error",
"message": f"Agent with name {name} not found",
}
return agent.tool_dict


if __name__ == "__main__":
mcp.run(transport='stdio')
Loading
Loading