Commit be234f51 authored by fushen's avatar fushen

Initial commit

parents
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="@localhost" uuid="48fd6c5e-3149-4657-9034-b7741bd917f5">
<driver-ref>redis</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>jdbc.RedisDriver</jdbc-driver>
<jdbc-url>jdbc:redis://localhost:6379/</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="1@localhost" uuid="a6a6e607-270c-4596-b92d-e7d1b2a189c3">
<driver-ref>redis</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>jdbc.RedisDriver</jdbc-driver>
<jdbc-url>jdbc:redis://localhost:6379/1</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
</profile>
</component>
\ No newline at end of file
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="fusion_agent" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="fusion_agent" project-jdk-type="Python SDK" />
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/fusion_agent.iml" filepath="$PROJECT_DIR$/.idea/fusion_agent.iml" />
</modules>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
from redis import Redis
from agent.llm.llm import AsyncQwen3Agent
from agent.llm.prompt import AGENT_PROMPT
from agent.memory.memory import RedisMemory
from agent.tools.tools import tool_manager, ToolManager, register_medical_tools,register_tools
API_KEY = "your_api_key"
# BASE_URL = "http://180.163.119.106:16031/v1"
BASE_URL = "http://180.163.119.106:16015/v1"
MODEL_NAME = "Fusion2-chat-v2.0"
#
#
# # #
# API_KEY = "sk-ba458ddd9fd649ea9442cc0461b496c6"
# BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
# # MODEL_NAME = "qwen3-235b-a22b" # 或者 qwen3模型名称
# MODEL_NAME = "qwen3-30b-a3b" # 或者 qwen3模型名称
# Redis 客户端建议复用,不用每次都重新创建
redis_client = Redis(host="localhost", port=6379, password="21221", decode_responses=True)
def create_agent(session_id: str) -> AsyncQwen3Agent:
CONFIG = {
# "FastGPT": {
# "url": "https://cloud.fastgpt.cn/api/mcp/app/oj7BgLJmYh45J17YxC7EEFAj/mcp"
# },
# "filesystem": {
# "args": [
# "-y",
# "@modelcontextprotocol/server-filesystem",
# "/tmp"
# ],
# "command": "npx"
# }
# "redis": {
# "command": "npx",
# "args": [
# "-y",
# "@modelcontextprotocol/server-redis",
# "redis://:21221@localhost:6379/1"
# ]
# }
}
tool_manager = ToolManager(CONFIG)
# register_medical_tools(tool_manager)
register_tools(tool_manager)
memory = RedisMemory(session_id=session_id, agent_id="weather", redis_client=redis_client)
agent = AsyncQwen3Agent(
api_key=API_KEY,
base_url=BASE_URL,
model_name=MODEL_NAME,
tool_manager=tool_manager,
memory=memory,
sys_prompt=AGENT_PROMPT,
# sys_prompt='/nothink'
)
return agent
import asyncio
async def main(session_id: str):
user_input = "查下病人张三的信息?并且查下上海的天气"
agent = create_agent(session_id)
await agent.tool_manager.initialize()
async for event in agent.stream(user_input=user_input):
if event["type"] == "content":
print(event["text"], end="", flush=True)
elif event["type"] == "think_start":
print("\n【模型思考开始】", end="", flush=True)
elif event["type"] == "think_continue":
print(event["text"], end="", flush=True)
elif event["type"] == "think_end":
print(event["text"], flush=True)
print("【模型思考结束】")
elif event["type"] == "tool_calls":
tool_info = event["info"]
print(f"\n【触发工具调用】函数:{tool_info}")
async for followup in agent.stream(tool_calls=tool_info):
if followup["type"] == "content":
print(followup["text"], end="", flush=True)
elif followup["type"] == "done":
print("\n【对话完成】")
return
elif event["type"] == "done":
print("\n【对话完成】")
return
if __name__ == "__main__":
asyncio.run(main("session423"))
AGENT_PROMPT = '''
你是一个具备调用外部工具能力的智能助手。你可以访问多个工具,每个工具都有功能描述、输入参数和输出返回值。
你的目标是根据用户请求**自动完成任务**,如数据查询、信息提取、结构分析、图表生成等。
若任务涉及多个工具,请严格按照以下执行流程自动规划和执行。
---
🔁【任务执行流程】:
1. 🎯 用户意图识别:
- 准确理解用户目标,如查询、对比、分析、汇总等;
- 若涉及数据库,重点识别用户意图所需的表、字段、条件、聚合需求;
2. 📊 构建工具依赖图:
- 根据任务目标自动识别所需工具;
- 构建有向依赖图,优先执行无依赖工具;
- 并发执行独立工具,串行执行有依赖链条的工具;
- 示例:获取表结构 ➜ 构造 SQL ➜ 执行 SQL
3. 🔐 数据库结构感知(防止幻觉):
- ⛔【禁止幻觉】:不得构造任何数据库中不存在的表名或字段名;
- ✅【结构验证流程】:
1. 自动执行 `SHOW TABLES` 获取所有表名;
2. 对目标表执行 `DESC 表名` 获取字段结构与类型;
3. 所有 SQL 构造必须基于已验证存在的表与字段;
4. 所有字段名应合法(不含中文、空格或特殊字符);
- 💡 若字段信息不足,不允许猜测,应主动向用户说明并补充请求;
- ✅ 字段类型感知:
- 构造 SQL 条件时,应根据字段类型判断是否加引号;
- 如:数字字段不加引号,字符串字段加引号;
4. 💾 SQL 安全与合法性要求:
- 所有 SQL 构造必须防止注入、拼接错误;
- 所有字段名、表名应转义合法;
- 不允许执行 DROP/TRUNCATE 等危险语句;
- 支持多条 SQL 时,需自动分句,确保并发时不会共享连接(避免 readexactly 错误);
5. 📦 多数据处理与分页:
- 默认在 SQL 中添加 `LIMIT` 限制(如 LIMIT 100);
- 若返回数据超出限制,应仅展示前若干条,并提示用户可分页查询;
- 支持添加分页参数(如 offset / page)构造 SQL;
6. 🔄 工具失败处理与重试策略:
- 工具调用失败时,若错误非结构性或系统错误,可自动重试一次;
- 若因参数错误或结构缺失导致失败,应修正参数后重试;
- 若返回值非结构化字符串,应尝试提取关键内容,必要时降级为提示文字;
7. ❓ 参数缺失时的主动提问:
- 若某工具必需参数缺失,且上下文无法推理,应合并为一句自然语言提问;
- 不要逐一提问多个字段,应尽量合并提示并减少对话轮数;
8. 📬 最终统一输出用户可读结果:
- 工具全部执行完成后,再统一返回最终结果;
- 查询任务应返回总数+样例数据;
- 中间调用过程默认不展示(如需展示需用户明确请求);
---
🧱【JSON 构造与格式规范】:
- ✅ 所有工具参数应为严格合法 JSON 格式;
- ✅ 所有字符串使用双引号 `"` 包裹;
- ✅ 不允许:
- 单引号 `'`;
- 尾逗号;
- 注释;
- 非法值如 `undefined`、`NaN`、`Infinity`;
- ✅ 支持嵌套 JSON,须逐级验证,确保 `JSON.parse()` 可成功解析;
- ❗禁止 Python 风格 dict、代码片段、含函数/表达式的结构;
- ✅ 可在工具调用前输出如:“以下 JSON 字符串已校验通过,可安全传入工具。”
---
🚫【禁止行为】:
- ❌ 跳过工具依赖直接调用;
- ❌ 使用未验证的表或字段构造 SQL(即禁止幻觉);
- ❌ 在结构未获取前构造查询;
- ❌ 返回无法被 JSON 解析的数据;
- ❌ 返回代码结构或工具细节(除非用户请求);
- ❌ 在工具调用失败后立即盲目重复相同调用;
---
📎【工具注册结构(供参考)】:
每个工具应提供如下元信息:
- 名称:唯一识别符;
- 描述:一句话说明功能;
- 输入参数(JSON Schema);
- 输出结构示例(JSON 格式);
---
✅【补充说明】:
- 工具调用过程可并发执行(如批量执行 `DESC`),避免串行拖慢响应;
- 若多个 SQL 查询依赖独立连接,应避免共享同一个连接(使用连接池或多连接并发);
- 所有调用任务均应以任务完成为目标,不应输出 Agent 的工具能力说明;
- 对于结构不满足的请求,应主动告知用户缺失信息,而非猜测生成;
'''
# memory/base.py
from abc import ABC, abstractmethod
from typing import List, Dict
class BaseMemory(ABC):
@abstractmethod
def load(self) -> List[Dict]: ...
@abstractmethod
def save(self, messages: List[Dict]): ...
@abstractmethod
def clear(self): ...
@abstractmethod
def append(self, message: Dict): ...
# memory/redis_memory.py
import json
class RedisMemory(BaseMemory):
def __init__(self, session_id: str, agent_id: str, redis_client):
self.key = f"memory:{session_id}:{agent_id}"
self.redis = redis_client
def load(self):
raw = self.redis.get(self.key)
if raw:
try:
return json.loads(raw)
except Exception as e:
print(f"[Memory Load Error] {e}")
return []
return []
def save(self, messages):
try:
self.redis.set(self.key, json.dumps(messages, ensure_ascii=False))
except Exception as e:
print(f"[Memory Save Error] {e}")
def clear(self):
self.redis.delete(self.key)
def append(self, message):
messages = self.load()
messages.append(message)
self.save(messages)
def extend(self, messages_to_add: list):
"""
批量追加多条消息
"""
messages = self.load()
messages.extend(messages_to_add)
self.save(messages)
def __repr__(self):
return f"<RedisMemory key={self.key} count={len(self.load())}>"
# ✅ 全局模拟 Redis 的共享内存字典
in_memory_store: Dict[str, List[Dict]] = {}
class InMemoryMemory(BaseMemory):
def __init__(self, session_id: str, agent_id: str):
self.key = f"memory:{session_id}:{agent_id}"
def load(self) -> List[Dict]:
return in_memory_store.get(self.key, []).copy()
def save(self, messages: List[Dict]):
in_memory_store[self.key] = messages.copy()
def clear(self):
in_memory_store.pop(self.key, None)
def append(self, message: Dict):
messages = in_memory_store.get(self.key, [])
messages.append(message)
in_memory_store[self.key] = messages
def extend(self, messages_to_add: List[Dict]):
messages = in_memory_store.get(self.key, [])
messages.extend(messages_to_add)
in_memory_store[self.key] = messages
def __repr__(self):
count = len(in_memory_store.get(self.key, []))
return f"<InMemoryMemory key={self.key} count={count}>"
\ No newline at end of file
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
import asyncio
import json
from agent.llm.llm import AsyncQwen3Agent
from agent.llm import main
class SessionManager:
def __init__(self, session_id: str):
self.session_id = session_id
self.state = "idle"
self.queue = asyncio.Queue()
self.main_agent: AsyncQwen3Agent = main.create_agent(session_id)
self.history = []
self.connections: set[WebSocket] = set() # 多个连接
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.connections.add(websocket)
def disconnect(self, websocket: WebSocket):
self.connections.discard(websocket)
async def broadcast(self, message: dict):
text = json.dumps(message)
for conn in self.connections.copy():
try:
await conn.send_text(text)
except Exception as e:
print(f"移除连接: {e}")
self.disconnect(conn)
async def _handle_stream(self, content: str = None, tool_calls: dict = None):
stream = (
self.main_agent.stream(user_input=content)
if content else
self.main_agent.stream(tool_calls=tool_calls)
)
async for event in stream:
event["session_id"] = self.session_id
# 推送当前事件
await self.queue.put(event)
await self.broadcast(event)
# 如果触发了工具调用,则递归继续处理
if event.get("type") == "tool_calls":
tool_info = event["info"]
print(f"\n【触发工具调用】函数:{tool_info}")
# 递归处理工具调用流
await self._handle_stream(tool_calls=tool_info)
async def start(self, content: str = None, tool_calls: dict = None):
self.state = "running"
try:
await self._handle_stream(content=content, tool_calls=tool_calls)
except Exception as e:
print(f"[错误] 处理流时出错: {e}")
self.state = "completed"
else:
self.state = "completed"
async def send_history_and_current(self, websocket: WebSocket):
for msg in self.history:
await websocket.send_text(json.dumps(msg))
# mcp_client/base.py
from abc import ABC, abstractmethod
from typing import Any
from abc import ABC, abstractmethod
from typing import Any
from abc import ABC, abstractmethod
from typing import Any
class BaseMcpClient(ABC):
def __init__(self, name: str, config: dict[str, Any]) -> None:
self.name = name
self.config = config
self._tools_cache: list[Any] | None = None # 缓存工具列表
async def __aenter__(self) -> "BaseMcpClient":
await self.initialize()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
await self.cleanup()
@abstractmethod
async def initialize(self) -> None:
...
@abstractmethod
async def list_tools(self) -> list[Any]:
...
@abstractmethod
async def call_tool(
self,
tool_name: str,
arguments: dict[str, Any],
retries: int = 2,
delay: float = 1.0,
) -> Any:
...
@abstractmethod
async def cleanup(self) -> None:
...
async def tools(self) -> list[Any]:
"""统一的工具列表访问接口,带缓存"""
if self._tools_cache is None:
tools_response = await self.list_tools()
tools = []
for tool in tools_response:
tools.append(
Tool(tool.name, tool.description, tool.inputSchema)
)
self._tools_cache = tools
return self._tools_cache
def clear_tool_cache(self) -> None:
"""手动清除工具缓存"""
self._tools_cache = None
from .stdio_client import StdioMcpClient
from .http_client import HttpMcpClient
from .base import BaseMcpClient
def create_mcp_client(name: str, config: dict[str, Any]) -> BaseMcpClient:
if "url" in config:
return HttpMcpClient(name, config)
elif "command" in config and "args" in config:
return StdioMcpClient(name, config)
else:
raise ValueError(f"Invalid config for MCP client '{name}': {config}")
class Tool:
"""Represents a tools with its properties and formatting."""
def __init__(
self, name: str, description: str, input_schema: dict[str, Any]
) -> None:
self.name: str = name
self.description: str = description
self.input_schema: dict[str, Any] = input_schema
def format_for_llm(self) -> str:
"""Format tools information for LLM.
Returns:
A formatted string describing the tools.
"""
args_desc = []
if "properties" in self.input_schema:
for param_name, param_info in self.input_schema["properties"].items():
arg_desc = (
f"- {param_name}: {param_info.get('description', 'No description')}"
)
if param_name in self.input_schema.get("required", []):
arg_desc += " (required)"
args_desc.append(arg_desc)
return f"""
Tool: {self.name}
Description: {self.description}
Arguments:
{chr(10).join(args_desc)}
"""
def to_dict(self) -> dict[str, Any]:
"""Serialize tools to OpenAI function calling schema."""
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": self.input_schema
}
}
class MCPClientProxy:
def __init__(self,tools):
self.tools = tools
def list_tools(self):
tools = []
for tool in self.tools:
tools.append({
"type":'function',
"function":{
"name":tool['name'],
"description":tool['description'],
"parameters":tool['inputSchema']
}
})
return tools
async def call_tool(self,tool_name,arguments):
from HotCode.core.globals import tool_set, tool_set_lock
for tool in self.tools:
if tool['name'] == tool_name:
async with tool_set_lock:
tool_client = tool_set.get(tool['toolSetId'])
if not tool_client:
raise Exception(f"{tool_name} 所属的工具集{tool['toolSetId']}不存在或未发布")
async with tool_client as client:
return await client.call_tool(tool_name,arguments)
return None
\ No newline at end of file
# mcp_client/http_client.py
import asyncio
import logging
from contextlib import AsyncExitStack
from typing import Any
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
from .base import BaseMcpClient
import asyncio
from typing import Any
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
from .base import BaseMcpClient
class HttpMcpClient(BaseMcpClient):
def __init__(self, name: str, config: dict[str, Any]) -> None:
super().__init__(name, config)
self._cleanup_lock = asyncio.Lock()
self._stream_ctx = None
self._session_ctx = None
self._stream = None
self.session: ClientSession | None = None
async def initialize(self) -> None:
if self.session:
return # 已初始化,跳过
url = self.config.get("url")
if not url:
raise ValueError("Missing 'url' in config.")
# 使用 AsyncExitStack 来统一管理异步上下文
self._exit_stack = AsyncExitStack()
stream_ctx = streamablehttp_client(url)
stream = await self._exit_stack.enter_async_context(stream_ctx)
read_stream, write_stream, _ = stream
session_ctx = ClientSession(read_stream, write_stream)
self.session = await self._exit_stack.enter_async_context(session_ctx)
await self.session.initialize()
async def list_tools(self) -> list[Any]:
if not self.session:
raise RuntimeError(f"Client {self.name} not initialized")
tools_response = await self.session.list_tools()
tools = []
for item in tools_response:
if isinstance(item, tuple) and item[0] == "tools":
tools.extend(item[1])
return tools
async def call_tool(
self,
tool_name: str,
arguments: dict[str, Any],
retries: int = 2,
delay: float = 1.0,
) -> Any:
url = self.config.get("url")
if not url:
raise ValueError("Missing 'url' in config.")
for attempt in range(1, retries + 1):
try:
async with streamablehttp_client(url) as (read_stream, write_stream, _):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
return (await session.call_tool(tool_name, arguments)).content[0].text
except Exception as e:
if attempt < retries:
await asyncio.sleep(delay)
else:
raise RuntimeError(f"Tool call failed after {retries} attempts: {e}") from e
async def cleanup(self) -> None:
async with self._cleanup_lock:
if self._exit_stack is not None:
await self._exit_stack.aclose()
self._exit_stack = None
self.session = None
import asyncio
import logging
from agent.tools.mcp.base import BaseMcpClient,create_mcp_client
# 配置日志
logging.basicConfig(level=logging.INFO)
# 示例配置:包含 HTTP 和 Stdio 客户端
CONFIG = {
"mcpServers": {
"FastGPT-mcp-6825886b058310fae2e58a3d": {
"url": "https://cloud.fastgpt.cn/api/mcp/app/og8ghYLRVsFMLi8GZ2nTFALn/mcp"
},
# "filesystem": {
# "command": "npx",
# "args": [
# "-y",
# "@modelcontextprotocol/server-filesystem",
# "C:\\Users\\byshe\\Desktop",
# ]
# },
# "FusionAI-mcp-d92407ae-ab04-4bce-991c-e1a603e5fe67": {
# "url": "http://192.168.120.59:4000/api/mcp_service/d92407ae-ab04-4bce-991c-e1a603e5fe67/mcp/"
# }
}
}
async def run():
clients: list[BaseMcpClient] = []
try:
# 初始化所有 MCP 客户端
for name, conf in CONFIG["mcpServers"].items():
client = create_mcp_client(name, conf)
await client.initialize()
logging.info(f"[{name}] Initialized.")
clients.append(client)
# 遍历客户端调用工具列表
for client in clients:
tools = await client.list_tools()
print(f"\n[{client.name}] 可用工具列表:")
for tool in tools:
print(f"- {tool}")
# # 示例:调用第一个工具(如果有)
# if tools:
# tool_name = tools[0].get("tool_name") if isinstance(tools[0], dict) else tools[0]
# result = await client.call_tool(tool_name, arguments={"query": "hello"})
# print(f"[{client.name}] 调用工具 {tool_name} 返回结果:\n{result}")
except Exception as e:
logging.error(f"运行中发生错误: {e}")
finally:
pass
# # 清理所有客户端资源
# for client in clients:
# await client.cleanup()
# logging.info(f"[{client.name}] 已清理资源.")
if __name__ == "__main__":
import logging
logging.getLogger("httpx").setLevel(logging.WARNING)
asyncio.run(run())
import asyncio
import logging
import traceback
from typing import Any
from agent.tools.mcp.base import BaseMcpClient, create_mcp_client
logger = logging.getLogger(__name__)
class McpProxy:
def __init__(self, config: dict[str, dict[str, Any]]):
self.config = config
self.clients: dict[str, BaseMcpClient] = {}
self.tool_map: dict[str, str] = {} # tool_name -> client_name
self.tool_cache: list[dict[str, Any]] = [] # 缓存聚合后的工具列表
async def initialize(self):
for name, conf in self.config.items():
client = create_mcp_client(name, conf)
await client.initialize()
logger.info(f"[{name}] 初始化成功")
self.clients[name] = client
await self._build_tool_cache()
async def _build_tool_cache(self):
"""构建工具列表缓存并建立 tool -> client 映射"""
self.tool_map.clear()
self.tool_cache.clear()
for name, client in self.clients.items():
try:
tools = await client.list_tools()
for tool in tools:
tool_name = tool.name
self.tool_map[tool_name] = name
self.tool_cache.append({
"type": 'function',
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.inputSchema
}
})
except Exception as e:
logger.warning(f"[{name}] 获取工具失败: {e}")
async def list_tools(self) -> list[dict[str, Any]]:
"""返回缓存中的工具列表,每项为 {'client': name, 'tool': tool_obj}"""
return self.tool_cache
async def refresh_tools(self):
"""强制重新获取所有工具信息并更新缓存"""
await self._build_tool_cache()
logger.info("工具列表已刷新")
async def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any:
"""自动查找并调用工具"""
try:
client_name = self.tool_map.get(tool_name)
if not client_name:
raise ValueError(f"未找到工具: {tool_name}")
client = self.clients[client_name]
logger.info(f"调用 [{client_name}] 的工具: {tool_name}")
res = await client.call_tool(tool_name, arguments)
except Exception as e:
traceback.print_exc()
raise e
return res
async def cleanup(self):
for name, client in self.clients.items():
await client.cleanup()
logger.info(f"[{name}] 已清理资源")
CONFIG = {
"FastGPT": {
"url": "https://cloud.fastgpt.cn/api/mcp/app/oj7BgLJmYh45J17YxC7EEFAj/mcp"
}
}
proxy = McpProxy(CONFIG)
if __name__ == "__main__":
import anyio
logging.basicConfig(level=logging.INFO)
logging.getLogger("httpx").setLevel(logging.WARNING)
async def main():
await proxy.initialize()
# 获取缓存中的所有工具
tools = await proxy.list_tools()
print(tools)
# await proxy.initialize()
# tools = await proxy.list_tools()
# print(tools)
# # 调用某个工具(会自动找)
result = await proxy.call_tool("计算器", {"question": "1+1", "a": 1, "b": 1, "operator": "add"})
print("调用结果:", result)
# 刷新工具列表缓存
# await proxy.refresh_tools()
#
# await proxy.cleanup()
anyio.run(main)
import asyncio
import logging
from contextlib import AsyncExitStack
from typing import Any
from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_client
from .base import BaseMcpClient
class StdioMcpClient(BaseMcpClient):
def __init__(self, name: str, config: dict[str, Any]) -> None:
super().__init__(name, config)
self.session: ClientSession | None = None
self.exit_stack: AsyncExitStack = AsyncExitStack()
self._cleanup_lock: asyncio.Lock = asyncio.Lock()
async def sampling_callback(self, message: types.CreateMessageRequestParams) -> types.CreateMessageResult:
return types.CreateMessageResult(
role="assistant",
content=types.TextContent(type="text", text="模拟模型响应"),
model="gpt-3.5-turbo",
stopReason="endTurn",
)
async def initialize(self) -> None:
command = self.config.get("command")
args = self.config.get("args")
env = self.config.get("env", None)
if not command or not args:
raise ValueError(f"Invalid stdio config for client '{self.name}'")
try:
server_params = StdioServerParameters(command=command, args=args, env=env)
read_stream, write_stream, *_ = await self.exit_stack.enter_async_context(
stdio_client(server_params)
)
self.session = await self.exit_stack.enter_async_context(
ClientSession(read_stream, write_stream, sampling_callback=self.sampling_callback)
)
await self.session.initialize()
except Exception as e:
logging.error(f"Error initializing Stdio client {self.name}: {e}")
await self.cleanup()
raise
async def list_tools(self) -> list[Any]:
if not self.session:
raise RuntimeError(f"Client {self.name} not initialized")
tools_response = await self.session.list_tools()
tools = []
for item in tools_response:
if isinstance(item, tuple) and item[0] == "tools":
tools.extend(item[1])
return tools
async def call_tool(
self,
tool_name: str,
arguments: dict[str, Any],
retries: int = 2,
delay: float = 1.0,
) -> Any:
if not self.session:
raise RuntimeError(f"Client {self.name} not initialized")
attempt = 0
while attempt < retries:
try:
res = await self.session.call_tool(tool_name, arguments)
return res.content[0].text
except Exception as e:
attempt += 1
logging.warning(f"[{self.name}] Error calling tool '{tool_name}': {e} (attempt {attempt})")
if attempt < retries:
await asyncio.sleep(delay)
else:
raise
async def cleanup(self) -> None:
async with self._cleanup_lock:
try:
# 关闭会话相关资源
if self.session:
# await self.session.close()
self.session = None
# 关闭 exit_stack 管理的资源
await self.exit_stack.aclose()
except asyncio.CancelledError:
self.session = None
except Exception as e:
logging.error(f"Error during cleanup of client {self.name}: {e}")
self.session = None
# 新增异步上下文管理器协议
async def __aenter__(self):
await self.initialize()
return self
async def __aexit__(self, exc_type, exc, tb):
await self.cleanup()
import asyncio
from mcp.client.streamable_http import streamablehttp_client
from mcp import ClientSession
async def main():
try:
# 连接到 streamable HTTP 服务端
async with streamablehttp_client("https://cloud.fastgpt.cn/api/mcp/app/og8ghYLRVsFMLi8GZ2nTFALn/mcp") as (
read_stream,
write_stream,
_
):
# 创建客户端会话
async with ClientSession(read_stream, write_stream) as session:
# 初始化连接
await session.initialize()
list = await session.list_tools()
print(list)
#
# # 调用工具(如 echo 工具)
# tool_result = await session.call_tool("echo", {"message": "hello"})
#
# print("Tool result:", tool_result)
except asyncio.CancelledError:
print("任务被取消")
except Exception as e:
print("发生错误:", e)
if __name__ == "__main__":
asyncio.run(main())
import json
raw = '{"option": "{\"title\": {\"text\": \"示例柱状图\"},\"xAxis\": {\"type\": \"category\",\"data\": [\"类别A\",\"类别B\",\"类别C\"]},\"yAxis\": {\"type\": \"value\"},\"series\": [{\"type\": \"bar\",\"data\": [120,200,150]}]}}'
data = json.loads(raw) # 解析外层JSON
option = json.loads(data['option']) # 再解析 option 字符串
print(option)
from typing import List, Union, Optional, Literal, Dict, Any
from pydantic import BaseModel, root_validator
from pydantic import model_validator
# ===== 基础结构类型 =====
class Title(BaseModel):
text: Optional[str] = None
left: Optional[str] = None
top: Optional[str] = None
class Tooltip(BaseModel):
trigger: Optional[str] = None
axisPointer: Optional[Dict[str, Any]] = None
class Legend(BaseModel):
orient: Optional[str] = None
left: Optional[str] = None
data: Optional[List[str]] = None
class Grid(BaseModel):
left: Optional[Union[str, int]] = None
right: Optional[Union[str, int]] = None
top: Optional[Union[str, int]] = None
bottom: Optional[Union[str, int]] = None
containLabel: Optional[bool] = None
class XAxis(BaseModel):
type: Optional[str] = None # category, value, time, log
data: Optional[List[Union[str, int, float]]] = None
class YAxis(BaseModel):
type: Optional[str] = None
class Radar(BaseModel):
indicator: Optional[List[Dict[str, Union[str, int]]]] = None
shape: Optional[str] = None
# ===== Series 类型(通用) =====
class BaseSeries(BaseModel):
type: Literal['bar', 'line', 'pie', 'scatter', 'radar', 'gauge', 'heatmap']
name: Optional[str] = None
data: Optional[List[Union[int, float, Dict[str, Any]]]] = None
smooth: Optional[bool] = None
stack: Optional[str] = None
radius: Optional[Union[str, List[str]]] = None # for pie
center: Optional[List[str]] = None # for pie
roseType: Optional[str] = None # for pie
itemStyle: Optional[Dict[str, Any]] = None
lineStyle: Optional[Dict[str, Any]] = None
areaStyle: Optional[Dict[str, Any]] = None
label: Optional[Dict[str, Any]] = None
emphasis: Optional[Dict[str, Any]] = None
min: Optional[float] = None # for gauge
max: Optional[float] = None # for gauge
pointer: Optional[Dict[str, Any]] = None # for gauge
# ===== 最终 Option 配置结构 =====
class EChartsOption(BaseModel):
title: Optional[Title] = None
tooltip: Optional[Tooltip] = None
legend: Optional[Legend] = None
grid: Optional[Union[Grid, List[Grid]]] = None
xAxis: Optional[Union[XAxis, List[XAxis]]] = None
yAxis: Optional[Union[YAxis, List[YAxis]]] = None
radar: Optional[Radar] = None
series: List[BaseSeries]
color: Optional[List[str]] = None
backgroundColor: Optional[Union[str, Dict[str, Any]]] = None
animation: Optional[bool] = None
textStyle: Optional[Dict[str, Any]] = None
toolbox: Optional[Dict[str, Any]] = None
dataset: Optional[Union[Dict[str, Any], List[Dict[str, Any]]]] = None
class Config:
extra = "allow" # 允许传递未声明字段
@model_validator(mode='after')
def check_series_exists(cls, values):
if not values.series or len(values.series) == 0:
raise ValueError("配置项中必须包含至少一个 series。")
return values
This diff is collapsed.
from .chat.router import router as chat_router
import uuid
from typing import Dict
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
import asyncio
import json
from agent.session import session
import importlib
router = APIRouter()
session_managers: Dict[str, session.SessionManager] = {}
@router.websocket("/ws/{session_id}")
async def websocket_handler(websocket: WebSocket, session_id: str):
if session_id not in session_managers:
importlib.reload(session)
session_managers[session_id] = session.SessionManager(session_id)
await session_managers[session_id].main_agent.tool_manager.initialize()
manager = session_managers[session_id]
await manager.connect(websocket)
try:
await manager.send_history_and_current(websocket)
while True:
text = await websocket.receive_text()
try:
data = json.loads(text)
except Exception:
await websocket.send_text(json.dumps({"error": "消息格式错误,必须是JSON"}))
continue
if manager.state == "running":
await websocket.send_text(json.dumps({"error": "任务正在执行中,请稍后再试"}))
continue
if data.get("action") == "start":
content = data.get("content", "默认内容")
# think = data.get("think", True)
manager.main_agent.cur_stream_id = uuid.uuid4().hex[:8]
asyncio.create_task(manager.start(content = content))
else:
await websocket.send_text(json.dumps({"error": "不支持的操作"}))
except WebSocketDisconnect:
print(f"[WebSocket] 断开: {session_id}")
manager.disconnect(websocket)
\ No newline at end of file
from fastapi import FastAPI
from app import *
app = FastAPI()
app.include_router(chat_router, prefix="/chat", tags=["chat"])
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
\ No newline at end of file
# Test your FastAPI endpoints
GET http://127.0.0.1:8000/
Accept: application/json
###
GET http://127.0.0.1:8000/hello/User
Accept: application/json
###
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment