From 0735d551c2d17191bf7d0fdf5270fbc6e242472e Mon Sep 17 00:00:00 2001 From: guy2015 Date: Sat, 12 Apr 2025 21:26:15 +0800 Subject: [PATCH 01/17] Add files via upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加mcp功能,对原项目的改动说明 --- GUY.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 GUY.md diff --git a/GUY.md b/GUY.md new file mode 100644 index 000000000..52aa6c2f8 --- /dev/null +++ b/GUY.md @@ -0,0 +1,3 @@ +1.增加了guymcp目录,下含mcp服务端和客户端,以及mcp的配置文件。 + +2 修改原项目 bot.chatgpt 下chat_gpt_bot.py文件,加入mcp客户端的应答。 From 8726926c35ebd78048103162c03a496ee0b7efd1 Mon Sep 17 00:00:00 2001 From: guy2015 Date: Sat, 12 Apr 2025 21:38:30 +0800 Subject: [PATCH 02/17] =?UTF-8?q?=E6=96=B0=E5=BB=BAguymcp=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=EF=BC=8C=E5=BC=95=E5=85=A5mcp=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- guymcp/.gitkeep | 1 + 1 file changed, 1 insertion(+) create mode 100644 guymcp/.gitkeep diff --git a/guymcp/.gitkeep b/guymcp/.gitkeep new file mode 100644 index 000000000..9c558e357 --- /dev/null +++ b/guymcp/.gitkeep @@ -0,0 +1 @@ +. From 891ce27311269e53ad0fed64860f22a975454751 Mon Sep 17 00:00:00 2001 From: guy2015 Date: Sat, 12 Apr 2025 21:41:13 +0800 Subject: [PATCH 03/17] =?UTF-8?q?mcp=E6=9C=8D=E5=8A=A1=E5=92=8C=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E5=8F=8A=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- guymcp/README.md | 43 ++ guymcp/client.py | 176 +++++ guymcp/contacts.txt | 5 + guymcp/pyproject_template.toml | 25 + guymcp/requirements.txt | 6 + guymcp/server.py | 1293 ++++++++++++++++++++++++++++++++ guymcp/tempbz.docx | Bin 0 -> 19177 bytes 7 files changed, 1548 insertions(+) create mode 100644 guymcp/README.md create mode 100644 guymcp/client.py create mode 100644 guymcp/contacts.txt create mode 100644 guymcp/pyproject_template.toml create mode 100644 guymcp/requirements.txt create mode 100644 guymcp/server.py create mode 100644 guymcp/tempbz.docx diff --git a/guymcp/README.md b/guymcp/README.md new file mode 100644 index 000000000..28c527819 --- /dev/null +++ b/guymcp/README.md @@ -0,0 +1,43 @@ +https://github.com/GobinFan/python-mcp-server-client/blob/main/README.md +# 创建项目目录 +uv init guymcp +cd guymcp + +# 创建并激活虚拟环境 +uv venv +source .venv/bin/activate # Windows: .venv\Scripts\activate + +# 安装依赖 +uv add "mcp[cli]" httpx +# 创建并激活虚拟环境 + +退出虚deactivate + +=============================文件说明 +.env 大模型连接配置文件 +server.py MCP服务端 +client.py MCP客户端 +requirements.txt 依赖包,通过pip3 install -r requirements.txt安装依赖 +contacts.txt 联系人测试数据 +tempbz.docx 考核表模板,用于测试 +pyproject_template.toml 修改父目录pyproject.toml的模板 +README.md 说明文件 + +=============================uv 安装,使用 +pip3 install -r requirements.txt +修改父目录pyproject.toml,加入[project]项目,不然uv run 会报错 +uv run server.py --host 127.0.0.1 --port 8020 +测试server curl -v http://127.0.0.1:8020/sse +uv run client.py http://127.0.0.1:8020/sse + +==============================不用uv 安装依赖的另一种方法.建议使用uv +pip3 install -r requirements.txt +python server.py --host 127.0.0.1 --port 8020 +python client.py http://127.0.0.1:8020/sse + +==============================功能演示 +按照安装说明,运行server.py,另开窗口运行client.py,输入提示词 +1.为程康生成公务员年度考核表,替换'name,sex,birthday,injob,party,dwzw,csgz'的值为'程康,男,1971.4,1989.12,无,国家工商总局重庆市江津区市场监管局第三所,企业监管' +2.查询张三电话号码 +3.查询所有姓李的电话 + diff --git a/guymcp/client.py b/guymcp/client.py new file mode 100644 index 000000000..f83c6ee10 --- /dev/null +++ b/guymcp/client.py @@ -0,0 +1,176 @@ +import asyncio +import json +import os +from typing import Optional +from contextlib import AsyncExitStack +import time +from mcp import ClientSession +from mcp.client.sse import sse_client + +from openai import AsyncOpenAI +from dotenv import load_dotenv + +load_dotenv() # load environment variables from .env + +class MCPClient: + def __init__(self): + # Initialize session and client objects + self.session: Optional[ClientSession] = None + self.exit_stack = AsyncExitStack() + # self.openai = AsyncOpenAI(api_key="sk-ausgzyjuyhyuaaizdxtzqltuimudowdrxwokgjrcgmebnwnm", base_url="https://api.siliconflow.cn/v1") + self.openai = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL")) + + async def connect_to_sse_server(self, server_url: str): + """Connect to an MCP server running with SSE transport""" + # Store the context managers so they stay alive + self._streams_context = sse_client(url=server_url) + streams = await self._streams_context.__aenter__() + + self._session_context = ClientSession(*streams) + self.session: ClientSession = await self._session_context.__aenter__() + + # Initialize + await self.session.initialize() + + # List available tools to verify connection + print("Initialized SSE client...") + print("Listing tools...") + response = await self.session.list_tools() + tools = response.tools + print("\nConnected to server with tools:", [tool.name for tool in tools]) + + async def cleanup(self): + """Properly clean up the session and streams""" + if self._session_context: + await self._session_context.__aexit__(None, None, None) + if self._streams_context: + await self._streams_context.__aexit__(None, None, None) + + async def process_query(self, query: str) -> str: + """Process a query using OpenAI API and available tools""" + messages = [ + { + "role": "user", + "content": query + } + ] + + response = await self.session.list_tools() + available_tools = [{ + "type": "function", + "function": { + "name": tool.name, + "description": tool.description, + "parameters": tool.inputSchema + } + } for tool in response.tools] + + # Initial OpenAI API call + completion = await self.openai.chat.completions.create( + model="Qwen/Qwen2.5-72B-Instruct", + # model=os.getenv("OPENAI_MODEL"), + max_tokens=1000, + messages=messages, + tools=available_tools + ) + + # Process response and handle tool calls + tool_results = [] + final_text = [] + + assistant_message = completion.choices[0].message + + if assistant_message.tool_calls: + for tool_call in assistant_message.tool_calls: + tool_name = tool_call.function.name + tool_args = json.loads(tool_call.function.arguments) + + # Execute tool call + result = await self.session.call_tool(tool_name, tool_args) + tool_results.append({"call": tool_name, "result": result}) + final_text.append(f"[Calling tool {tool_name} with args {tool_args}]") + + # Continue conversation with tool results + messages.extend([ + { + "role": "assistant", + "content": None, + "tool_calls": [tool_call] + }, + { + "role": "tool", + "tool_call_id": tool_call.id, + "content": result.content[0].text + } + ]) + + print(f"Tool {tool_name} returned: {result.content[0].text}") + print("messages", messages) + # Get next response from OpenAI + completion = await self.openai.chat.completions.create( + model=os.getenv("OPENAI_MODEL"), + max_tokens=1000, + messages=messages, + ) + if isinstance(completion.choices[0].message.content, (dict, list)): + final_text.append(str(completion.choices[0].message.content)) + else: + final_text.append(completion.choices[0].message.content) + else: + if isinstance(assistant_message.content, (dict, list)): + final_text.append(str(assistant_message.content)) + else: + final_text.append(assistant_message.content) + + return "\n".join(final_text) + + async def chat_loop(self): + """Run an interactive chat loop""" + print("\nMCP Client Started!") + print("Type your queries or 'quit' to exit.") + + while True: + try: + query = input("\nQuery: ").strip() + + if query.lower() == 'quit': + break + + response = await self.process_query(query) + print("\n" + response) + + except Exception as e: + print(f"\nError: {str(e)}") + async def gychat_loop(self,querystr) -> str: + """Run an interactive chat loop""" + print("\nMCP Client Started!") + print("Type your queries or 'quit' to exit.") + response = "响应初始化..." + try: + # query = input("\nQuery: ").strip() + query = querystr + if query.lower() == 'quit': + return + + response = await self.process_query(query) + print("\n" + response) + + except Exception as e: + print(f"\nError: {str(e)}") + finally: + return response +async def main(): + if len(sys.argv) < 2: + print("Usage: uv run client.py ") + sys.exit(1) + + client = MCPClient() + try: + await client.connect_to_sse_server(server_url=sys.argv[1]) + await client.chat_loop() + finally: + await client.cleanup() + +if __name__ == "__main__": + import sys + asyncio.run(main()) \ No newline at end of file diff --git a/guymcp/contacts.txt b/guymcp/contacts.txt new file mode 100644 index 000000000..b45485592 --- /dev/null +++ b/guymcp/contacts.txt @@ -0,0 +1,5 @@ +张三 | 13996048383 | 北京市海淀区 | 技术部主管 +李四 | 13652589299 | 北京市海淀区 | 技术部主管 +王麻子 | 13500353307 | 北京市海淀区 | 技术部主管 +李二 | 13512352336 | 北京市海淀区 | 技术部主管 +李三 | 13668019077 | 北京市海淀区 | 技术部主管 diff --git a/guymcp/pyproject_template.toml b/guymcp/pyproject_template.toml new file mode 100644 index 000000000..be2c003cf --- /dev/null +++ b/guymcp/pyproject_template.toml @@ -0,0 +1,25 @@ +[project] +name = "guychat01" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "bs4>=0.0.2", + "httpx>=0.28.1", + "mcp[cli]>=1.6.0", + "openai>=1.70.0", + "python-docx>=1.1.2", + "requests>=2.32.3", +] +[tool.black] +line-length = 176 +target-version = ['py37'] +include = '\.pyi?$' +extend-exclude = '.+/(dist|.venv|venv|build|lib)/.+' + +[tool.isort] +profile = "black" + +[tool.uv.workspace] +members = ["mcp-server"] \ No newline at end of file diff --git a/guymcp/requirements.txt b/guymcp/requirements.txt new file mode 100644 index 000000000..e984ae6e8 --- /dev/null +++ b/guymcp/requirements.txt @@ -0,0 +1,6 @@ +bs4>=0.0.2 +httpx>=0.28.1 +mcp[cli]>=1.6.0 +openai>=1.70.0 +python-docx>=1.1.2 +requests>=2.32.3 diff --git a/guymcp/server.py b/guymcp/server.py new file mode 100644 index 000000000..5bdca9df4 --- /dev/null +++ b/guymcp/server.py @@ -0,0 +1,1293 @@ +from mcp.server.fastmcp import FastMCP +from dotenv import load_dotenv +import httpx +import json +import os +from bs4 import BeautifulSoup +from typing import Any +import httpx +from mcp.server.fastmcp import FastMCP +from starlette.applications import Starlette +from mcp.server.sse import SseServerTransport +from starlette.requests import Request +from starlette.routing import Mount, Route +from mcp.server import Server +import uvicorn + +import os +import io +import base64 +import shutil +from typing import Dict, List, Optional, Any, Union, Tuple +import json +from docx import Document +from docx.shared import Pt, Inches, RGBColor +from docx.enum.text import WD_PARAGRAPH_ALIGNMENT +from docx.enum.table import WD_TABLE_ALIGNMENT +from docx.enum.style import WD_STYLE_TYPE +from mcp.server.fastmcp import FastMCP +from docx.enum.text import WD_COLOR_INDEX +from docx.oxml.shared import OxmlElement, qn +from docx.oxml.ns import nsdecls +from docx.oxml import parse_xml +import sys +from openai import OpenAI +import re # 添加正则表达式模块 + +load_dotenv() + +mcp = FastMCP("docs") + +USER_AGENT = "docs-app/1.0" +SERPER_URL = "https://google.serper.dev/search" + +docs_urls = { + "langchain": "python.langchain.com/docs", + "llama-index": "docs.llamaindex.ai/en/stable", + "autogen": "microsoft.github.io/autogen/stable", + "agno": "docs.agno.com", + "openai-agents-sdk": "openai.github.io/openai-agents-python", + "mcp-doc": "modelcontextprotocol.io", + "camel-ai": "docs.camel-ai.org", + "crew-ai": "docs.crewai.com" +} + +async def search_web(query: str) -> dict | None: + payload = json.dumps({"q": query, "num": 2}) + + headers = { + "X-API-KEY": os.getenv("SERPER_API_KEY"), + "Content-Type": "application/json", + } + + async with httpx.AsyncClient() as client: + try: + response = await client.post( + SERPER_URL, headers=headers, data=payload, timeout=30.0 + ) + response.raise_for_status() + return response.json() + except httpx.TimeoutException: + return {"organic": []} + +async def fetch_url(url: str): + async with httpx.AsyncClient() as client: + try: + response = await client.get(url, timeout=30.0) + soup = BeautifulSoup(response.text, "html.parser") + text = soup.get_text() + return text + except httpx.TimeoutException: + return "Timeout error" + +@mcp.tool() +async def get_docs(query: str, library: str): + """ + 搜索给定查询和库的最新文档。 + 支持 langchain、llama-index、autogen、agno、openai-agents-sdk、mcp-doc、camel-ai 和 crew-ai。 + + 参数: + query: 要搜索的查询 (例如 "React Agent") + library: 要搜索的库 (例如 "agno") + + 返回: + 文档中的文本 + """ + if library not in docs_urls: + raise ValueError(f"Library {library} not supported by this tool") + + query = f"site:{docs_urls[library]} {query}" + results = await search_web(query) + if len(results["organic"]) == 0: + return "No results found" + + text = "" + for result in results["organic"]: + text += await fetch_url(result["link"]) + + return text + +@mcp.tool(description="查询符合输入姓名的人的电话号码") +def queryphone(a: str) -> dict: + """ + 查询某人的电话号码 + Args: + a (str): 第一个字符串 + Returns: + dict: 包含查询结果的字典 + + """ + + result: dict + result = cxphone(a) + return result + +def cxphone(name: str) -> dict: + """ + 查找并返回contacts.txt中指定名字的行 + Args: + name (str): 要查找的名字 + Returns: + dict: 包含名字和电话号码的字典列表 + """ + results = [] + with open('contacts.txt', 'r', encoding='utf-8') as file: + for line in file: + parts = line.strip().split('|') + # 去掉所有的空格 + parts = [part.strip() for part in parts] + if len(parts) >= 2 and name in parts[0]: # 修改条件为包含name + results.append({"name": parts[0], "phone": parts[1]}) + if results: + return {"results": results} + else: + return {"error": f"未找到{name}的相关信息"} + +#guy +documents = {} + +# Helper Functions +def get_document_properties(doc_path: str) -> Dict[str, Any]: + """Get properties of a Word document.""" + if not os.path.exists(doc_path): + return {"error": f"Document {doc_path} does not exist"} + + try: + doc = Document(doc_path) + core_props = doc.core_properties + + return { + "title": core_props.title or "", + "author": core_props.author or "", + "subject": core_props.subject or "", + "keywords": core_props.keywords or "", + "created": str(core_props.created) if core_props.created else "", + "modified": str(core_props.modified) if core_props.modified else "", + "last_modified_by": core_props.last_modified_by or "", + "revision": core_props.revision or 0, + "page_count": len(doc.sections), + "word_count": sum(len(paragraph.text.split()) for paragraph in doc.paragraphs), + "paragraph_count": len(doc.paragraphs), + "table_count": len(doc.tables) + } + except Exception as e: + return {"error": f"Failed to get document properties: {str(e)}"} + +def extract_document_text(doc_path: str) -> str: + """Extract all text from a Word document.""" + if not os.path.exists(doc_path): + return f"Document {doc_path} does not exist" + + try: + doc = Document(doc_path) + text = [] + + for paragraph in doc.paragraphs: + text.append(paragraph.text) + + for table in doc.tables: + for row in table.rows: + for cell in row.cells: + for paragraph in cell.paragraphs: + text.append(paragraph.text) + + return "\n".join(text) + except Exception as e: + return f"Failed to extract text: {str(e)}" + +def get_document_structure(doc_path: str) -> Dict[str, Any]: + """Get the structure of a Word document.""" + if not os.path.exists(doc_path): + return {"error": f"Document {doc_path} does not exist"} + + try: + doc = Document(doc_path) + structure = { + "paragraphs": [], + "tables": [] + } + + # Get paragraphs + for i, para in enumerate(doc.paragraphs): + structure["paragraphs"].append({ + "index": i, + "text": para.text[:100] + ("..." if len(para.text) > 100 else ""), + "style": para.style.name if para.style else "Normal" + }) + + # Get tables + for i, table in enumerate(doc.tables): + table_data = { + "index": i, + "rows": len(table.rows), + "columns": len(table.columns), + "preview": [] + } + + # Get sample of table data + max_rows = min(3, len(table.rows)) + for row_idx in range(max_rows): + row_data = [] + max_cols = min(3, len(table.columns)) + for col_idx in range(max_cols): + try: + cell_text = table.cell(row_idx, col_idx).text + row_data.append(cell_text[:20] + ("..." if len(cell_text) > 20 else "")) + except IndexError: + row_data.append("N/A") + table_data["preview"].append(row_data) + + structure["tables"].append(table_data) + + return structure + except Exception as e: + return {"error": f"Failed to get document structure: {str(e)}"} + +def check_file_writeable(filepath: str) -> Tuple[bool, str]: + """ + Check if a file can be written to. + + Args: + filepath: Path to the file + + Returns: + Tuple of (is_writeable, error_message) + """ + # If file doesn't exist, check if directory is writeable + if not os.path.exists(filepath): + directory = os.path.dirname(filepath) + if not os.path.exists(directory): + return False, f"Directory {directory} does not exist" + if not os.access(directory, os.W_OK): + return False, f"Directory {directory} is not writeable" + return True, "" + + # If file exists, check if it's writeable + if not os.access(filepath, os.W_OK): + return False, f"File {filepath} is not writeable (permission denied)" + + # Try to open the file for writing to see if it's locked + try: + with open(filepath, 'a'): + pass + return True, "" + except IOError as e: + return False, f"File {filepath} is not writeable: {str(e)}" + except Exception as e: + return False, f"Unknown error checking file permissions: {str(e)}" + +def create_document_copy(source_path: str, dest_path: Optional[str] = None) -> Tuple[bool, str, Optional[str]]: + """ + Create a copy of a document. + + Args: + source_path: Path to the source document + dest_path: Optional path for the new document. If not provided, will use source_path + '_copy.docx' + + Returns: + Tuple of (success, message, new_filepath) + """ + if not os.path.exists(source_path): + return False, f"Source document {source_path} does not exist", None + + if not dest_path: + # Generate a new filename if not provided + base, ext = os.path.splitext(source_path) + dest_path = f"{base}_copy{ext}" + + try: + # Simple file copy + shutil.copy2(source_path, dest_path) + return True, f"Document copied to {dest_path}", dest_path + except Exception as e: + return False, f"Failed to copy document: {str(e)}", None + +def ensure_heading_style(doc): + """ + Ensure Heading styles exist in the document. + + Args: + doc: Document object + """ + for i in range(1, 10): # Create Heading 1 through Heading 9 + style_name = f'Heading {i}' + try: + # Try to access the style to see if it exists + style = doc.styles[style_name] + except KeyError: + # Create the style if it doesn't exist + try: + style = doc.styles.add_style(style_name, WD_STYLE_TYPE.PARAGRAPH) + if i == 1: + style.font.size = Pt(16) + style.font.bold = True + elif i == 2: + style.font.size = Pt(14) + style.font.bold = True + else: + style.font.size = Pt(12) + style.font.bold = True + except Exception: + # If style creation fails, we'll just use default formatting + pass + +def ensure_table_style(doc): + """ + Ensure Table Grid style exists in the document. + + Args: + doc: Document object + """ + try: + # Try to access the style to see if it exists + style = doc.styles['Table Grid'] + except KeyError: + # If style doesn't exist, we'll handle it at usage time + pass + +# MCP Tools +@mcp.tool() +async def create_document(filename: str, title: Optional[str] = None, author: Optional[str] = None) -> str: + """Create a new Word document with optional metadata. + + Args: + filename: Name of the document to create (with or without .docx extension) + title: Optional title for the document metadata + author: Optional author for the document metadata + """ + if not filename.endswith('.docx'): + filename += '.docx' + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot create document: {error_message}" + + try: + doc = Document() + + # Set properties if provided + if title: + doc.core_properties.title = title + if author: + doc.core_properties.author = author + + # Ensure necessary styles exist + ensure_heading_style(doc) + ensure_table_style(doc) + + # Save the document + doc.save(filename) + + return f"Document {filename} created successfully" + except Exception as e: + return f"Failed to create document: {str(e)}" + +@mcp.tool() +async def add_heading(filename: str, text: str, level: int = 1) -> str: + """Add a heading to a Word document. + + Args: + filename: Path to the Word document + text: Heading text + level: Heading level (1-9, where 1 is the highest level) + """ + if not filename.endswith('.docx'): + filename += '.docx' + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + # Suggest creating a copy + return f"Cannot modify document: {error_message}. Consider creating a copy first or creating a new document." + + try: + doc = Document(filename) + + # Ensure heading styles exist + ensure_heading_style(doc) + + # Try to add heading with style + try: + heading = doc.add_heading(text, level=level) + doc.save(filename) + return f"Heading '{text}' (level {level}) added to {filename}" + except Exception as style_error: + # If style-based approach fails, use direct formatting + paragraph = doc.add_paragraph(text) + paragraph.style = doc.styles['Normal'] + run = paragraph.runs[0] + run.bold = True + # Adjust size based on heading level + if level == 1: + run.font.size = Pt(16) + elif level == 2: + run.font.size = Pt(14) + else: + run.font.size = Pt(12) + + doc.save(filename) + return f"Heading '{text}' added to {filename} with direct formatting (style not available)" + except Exception as e: + return f"Failed to add heading: {str(e)}" + +@mcp.tool() +async def add_paragraph(filename: str, text: str, style: Optional[str] = None) -> str: + """Add a paragraph to a Word document. + + Args: + filename: Path to the Word document + text: Paragraph text + style: Optional paragraph style name + """ + if not filename.endswith('.docx'): + filename += '.docx' + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + # Suggest creating a copy + return f"Cannot modify document: {error_message}. Consider creating a copy first or creating a new document." + + try: + doc = Document(filename) + paragraph = doc.add_paragraph(text) + + if style: + try: + paragraph.style = style + except KeyError: + # Style doesn't exist, use normal and report it + paragraph.style = doc.styles['Normal'] + doc.save(filename) + return f"Style '{style}' not found, paragraph added with default style to {filename}" + + doc.save(filename) + return f"Paragraph added to {filename}" + except Exception as e: + return f"Failed to add paragraph: {str(e)}" + +@mcp.tool() +async def add_table(filename: str, rows: int, cols: int, data: Optional[List[List[str]]] = None) -> str: + """Add a table to a Word document. + + Args: + filename: Path to the Word document + rows: Number of rows in the table + cols: Number of columns in the table + data: Optional 2D array of data to fill the table + """ + if not filename.endswith('.docx'): + filename += '.docx' + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + # Suggest creating a copy + return f"Cannot modify document: {error_message}. Consider creating a copy first or creating a new document." + + try: + doc = Document(filename) + table = doc.add_table(rows=rows, cols=cols) + + # Try to set the table style + try: + table.style = 'Table Grid' + except KeyError: + # If style doesn't exist, add basic borders + # This is a simplified approach - complete border styling would require more code + pass + + # Fill table with data if provided + if data: + for i, row_data in enumerate(data): + if i >= rows: + break + for j, cell_text in enumerate(row_data): + if j >= cols: + break + table.cell(i, j).text = str(cell_text) + + doc.save(filename) + return f"Table ({rows}x{cols}) added to {filename}" + except Exception as e: + return f"Failed to add table: {str(e)}" + +@mcp.tool() +async def add_picture(filename: str, image_path: str, width: Optional[float] = None) -> str: + """Add an image to a Word document. + + Args: + filename: Path to the Word document + image_path: Path to the image file + width: Optional width in inches (proportional scaling) + """ + if not filename.endswith('.docx'): + filename += '.docx' + + # Validate document existence + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Get absolute paths for better diagnostics + abs_filename = os.path.abspath(filename) + abs_image_path = os.path.abspath(image_path) + + # Validate image existence with improved error message + if not os.path.exists(abs_image_path): + return f"Image file not found: {abs_image_path}" + + # Check image file size + try: + image_size = os.path.getsize(abs_image_path) / 1024 # Size in KB + if image_size <= 0: + return f"Image file appears to be empty: {abs_image_path} (0 KB)" + except Exception as size_error: + return f"Error checking image file: {str(size_error)}" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(abs_filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first or creating a new document." + + try: + doc = Document(abs_filename) + # Additional diagnostic info + diagnostic = f"Attempting to add image ({abs_image_path}, {image_size:.2f} KB) to document ({abs_filename})" + + try: + if width: + doc.add_picture(abs_image_path, width=Inches(width)) + else: + doc.add_picture(abs_image_path) + doc.save(abs_filename) + return f"Picture {image_path} added to {filename}" + except Exception as inner_error: + # More detailed error for the specific operation + error_type = type(inner_error).__name__ + error_msg = str(inner_error) + return f"Failed to add picture: {error_type} - {error_msg or 'No error details available'}\nDiagnostic info: {diagnostic}" + except Exception as outer_error: + # Fallback error handling + error_type = type(outer_error).__name__ + error_msg = str(outer_error) + return f"Document processing error: {error_type} - {error_msg or 'No error details available'}" + +@mcp.tool() +async def get_document_info(filename: str) -> str: + """Get information about a Word document. + + Args: + filename: Path to the Word document + """ + if not filename.endswith('.docx'): + filename += '.docx' + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + try: + properties = get_document_properties(filename) + return json.dumps(properties, indent=2) + except Exception as e: + return f"Failed to get document info: {str(e)}" + +@mcp.tool() +async def get_document_text(filename: str) -> str: + """Extract all text from a Word document. + + Args: + filename: Path to the Word document + """ + if not filename.endswith('.docx'): + filename += '.docx' + + return extract_document_text(filename) + +@mcp.tool() +async def get_document_outline(filename: str) -> str: + """Get the structure of a Word document. + + Args: + filename: Path to the Word document + """ + if not filename.endswith('.docx'): + filename += '.docx' + + structure = get_document_structure(filename) + return json.dumps(structure, indent=2) + +@mcp.tool() +async def list_available_documents(directory: str = ".") -> str: + """List all .docx files in the specified directory. + + Args: + directory: Directory to search for Word documents + """ + try: + if not os.path.exists(directory): + return f"Directory {directory} does not exist" + + docx_files = [f for f in os.listdir(directory) if f.endswith('.docx')] + + if not docx_files: + return f"No Word documents found in {directory}" + + result = f"Found {len(docx_files)} Word documents in {directory}:\n" + for file in docx_files: + file_path = os.path.join(directory, file) + size = os.path.getsize(file_path) / 1024 # KB + result += f"- {file} ({size:.2f} KB)\n" + + return result + except Exception as e: + return f"Failed to list documents: {str(e)}" + +@mcp.tool() +async def copy_document(source_filename: str, destination_filename: Optional[str] = None) -> str: + """Create a copy of a Word document. + + Args: + source_filename: Path to the source document + destination_filename: Optional path for the copy. If not provided, a default name will be generated. + """ + if not source_filename.endswith('.docx'): + source_filename += '.docx' + + if destination_filename and not destination_filename.endswith('.docx'): + destination_filename += '.docx' + + success, message, new_path = create_document_copy(source_filename, destination_filename) + if success: + return message + else: + return f"Failed to copy document: {message}" + +# Resources +@mcp.resource("docx:{path}") +async def document_resource(path: str) -> str: + """Access Word document content.""" + if not path.endswith('.docx'): + path += '.docx' + + if not os.path.exists(path): + return f"Document {path} does not exist" + + return extract_document_text(path) +def find_paragraph_by_text(doc, text, partial_match=False): + """ + Find paragraphs containing specific text. + + Args: + doc: Document object + text: Text to search for + partial_match: If True, matches paragraphs containing the text; if False, matches exact text + + Returns: + List of paragraph indices that match the criteria + """ + matching_paragraphs = [] + + for i, para in enumerate(doc.paragraphs): + if partial_match and text in para.text: + matching_paragraphs.append(i) + elif not partial_match and para.text == text: + matching_paragraphs.append(i) + + return matching_paragraphs + +def find_and_replace_text(doc, old_text, new_text): + """ + Find and replace text throughout the document. + + Args: + doc: Document object + old_text: Text to find + new_text: Text to replace with + + Returns: + Number of replacements made + """ + count = 0 + + # Search in paragraphs + for para in doc.paragraphs: + if old_text in para.text: + for run in para.runs: + if old_text in run.text: + run.text = run.text.replace(old_text, new_text) + count += 1 + + # Search in tables + for table in doc.tables: + for row in table.rows: + for cell in row.cells: + for para in cell.paragraphs: + if old_text in para.text: + for run in para.runs: + if old_text in run.text: + run.text = run.text.replace(old_text, new_text) + count += 1 + + return count + +def set_cell_border(cell, **kwargs): + """ + Set cell border properties. + + Args: + cell: The cell to modify + **kwargs: Border properties (top, bottom, left, right, val, color) + """ + tc = cell._tc + tcPr = tc.get_or_add_tcPr() + + # Create border elements + for key, value in kwargs.items(): + if key in ['top', 'left', 'bottom', 'right']: + tag = 'w:{}'.format(key) + + element = OxmlElement(tag) + element.set(qn('w:val'), kwargs.get('val', 'single')) + element.set(qn('w:sz'), kwargs.get('sz', '4')) + element.set(qn('w:space'), kwargs.get('space', '0')) + element.set(qn('w:color'), kwargs.get('color', 'auto')) + + tcBorders = tcPr.first_child_found_in("w:tcBorders") + if tcBorders is None: + tcBorders = OxmlElement('w:tcBorders') + tcPr.append(tcBorders) + + tcBorders.append(element) + +def create_style(doc, style_name, style_type, base_style=None, font_properties=None, paragraph_properties=None): + """ + Create a new style in the document. + + Args: + doc: Document object + style_name: Name for the new style + style_type: Type of style (WD_STYLE_TYPE) + base_style: Optional base style to inherit from + font_properties: Dictionary of font properties (bold, italic, size, name, color) + paragraph_properties: Dictionary of paragraph properties (alignment, spacing) + + Returns: + The created style + """ + try: + # Check if style already exists + style = doc.styles.get_by_id(style_name, WD_STYLE_TYPE.PARAGRAPH) + return style + except: + # Create new style + new_style = doc.styles.add_style(style_name, style_type) + + # Set base style if specified + if base_style: + new_style.base_style = doc.styles[base_style] + + # Set font properties + if font_properties: + font = new_style.font + if 'bold' in font_properties: + font.bold = font_properties['bold'] + if 'italic' in font_properties: + font.italic = font_properties['italic'] + if 'size' in font_properties: + font.size = Pt(font_properties['size']) + if 'name' in font_properties: + font.name = font_properties['name'] + if 'color' in font_properties: + try: + # For RGB color + font.color.rgb = font_properties['color'] + except: + # For named color + font.color.theme_color = font_properties['color'] + + # Set paragraph properties + if paragraph_properties: + if 'alignment' in paragraph_properties: + new_style.paragraph_format.alignment = paragraph_properties['alignment'] + if 'spacing' in paragraph_properties: + new_style.paragraph_format.line_spacing = paragraph_properties['spacing'] + + return new_style + +# Add these MCP tools to the existing set + +@mcp.tool() +async def format_text(filename: str, paragraph_index: int, start_pos: int, end_pos: int, + bold: Optional[bool] = None, italic: Optional[bool] = None, + underline: Optional[bool] = None, color: Optional[str] = None, + font_size: Optional[int] = None, font_name: Optional[str] = None) -> str: + """Format a specific range of text within a paragraph. + + Args: + filename: Path to the Word document + paragraph_index: Index of the paragraph (0-based) + start_pos: Start position within the paragraph text + end_pos: End position within the paragraph text + bold: Set text bold (True/False) + italic: Set text italic (True/False) + underline: Set text underlined (True/False) + color: Text color (e.g., 'red', 'blue', etc.) + font_size: Font size in points + font_name: Font name/family + """ + if not filename.endswith('.docx'): + filename += '.docx' + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate paragraph index + if paragraph_index < 0 or paragraph_index >= len(doc.paragraphs): + return f"Invalid paragraph index. Document has {len(doc.paragraphs)} paragraphs (0-{len(doc.paragraphs)-1})." + + paragraph = doc.paragraphs[paragraph_index] + text = paragraph.text + + # Validate text positions + if start_pos < 0 or end_pos > len(text) or start_pos >= end_pos: + return f"Invalid text positions. Paragraph has {len(text)} characters." + + # Get the text to format + target_text = text[start_pos:end_pos] + + # Clear existing runs and create three runs: before, target, after + for run in paragraph.runs: + run.clear() + + # Add text before target + if start_pos > 0: + run_before = paragraph.add_run(text[:start_pos]) + + # Add target text with formatting + run_target = paragraph.add_run(target_text) + if bold is not None: + run_target.bold = bold + if italic is not None: + run_target.italic = italic + if underline is not None: + run_target.underline = underline + if color: + try: + # Try to set color by name + run_target.font.color.rgb = RGBColor.from_string(color) + except: + # If color name doesn't work, try predefined colors + color_map = { + 'red': WD_COLOR_INDEX.RED, + 'blue': WD_COLOR_INDEX.BLUE, + 'green': WD_COLOR_INDEX.GREEN, + 'yellow': WD_COLOR_INDEX.YELLOW, + 'black': WD_COLOR_INDEX.BLACK, + } + if color.lower() in color_map: + run_target.font.color.index = color_map[color.lower()] + if font_size: + run_target.font.size = Pt(font_size) + if font_name: + run_target.font.name = font_name + + # Add text after target + if end_pos < len(text): + run_after = paragraph.add_run(text[end_pos:]) + + doc.save(filename) + return f"Text '{target_text}' formatted successfully in paragraph {paragraph_index}." + except Exception as e: + return f"Failed to format text: {str(e)}" + +@mcp.tool() +async def search_and_replace(filename: str, find_text: str, replace_text: str) -> str: + """Search for text and replace all occurrences. + + Args: + filename: Path to the Word document + find_text: Text to search for + replace_text: Text to replace with + """ + if not filename.endswith('.docx'): + filename += '.docx' + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Perform find and replace + count = find_and_replace_text(doc, find_text, replace_text) + + if count > 0: + doc.save(filename) + return f"Replaced {count} occurrence(s) of '{find_text}' with '{replace_text}'." + else: + return f"No occurrences of '{find_text}' found." + except Exception as e: + return f"Failed to search and replace: {str(e)}" + +# guy 得到个人总结 +async def gy_get_grzj(question: str)-> str: + client = OpenAI( + base_url='https://api.siliconflow.cn/v1', + api_key='sk-ausgzyjuyhyuaaizdxtzqltuimudowdrxwokgjrcgmebnwnm' + ) + + # 发送带有流式输出的请求 + response = client.chat.completions.create( + # model="deepseek-ai/DeepSeek-V2.5", + model="Qwen/Qwen2.5-72B-Instruct", + messages=[ + {"role": "user", "content": question} + ] + ) + # 解析返回内容 + if response.choices: + # 提取代码块(带Markdown格式检测) + answer = response.choices[0].message.content + return answer + else: + answer = "未获得有效响应" + return answer + +#生成公务员年度考核表 +@mcp.tool() +async def gy_genetate_file(filename: str, find_text: str, replace_text: str) -> str: + """生成指定姓名人员的公务员年度考核表. + + Args: + filename: 指定的人员的姓名 + find_text: Text to search for + replace_text: Text to replace with + """ + await copy_document('tempbz.docx', filename +'.docx') + if not filename.endswith('.docx'): + filename += '.docx' + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # 使用正则表达式分割 find_text 和 replace_text + find_texts = re.split(r'[,\s,]+', find_text.strip()) + replace_texts = re.split(r'[,\s,]+', replace_text.strip()) + + # Ensure both lists have the same length + if len(find_texts) != len(replace_texts): + return f"Error: Number of find texts ({len(find_texts)}) does not match number of replace texts ({len(replace_texts)})." + + # Perform find and replace for each pair + total_count = 0 + for find, replace in zip(find_texts, replace_texts): + count = find_and_replace_text(doc, find.strip(), replace.strip()) + total_count += count + + # guy + zj = await gy_get_grzj('写230字到234字的个人年度工作总结,身份是基层市场监管所一般工作人员,负责食品安全相关作') + cleaned_zj = zj.replace("\r\n", "\n") + + # 新增过滤逻辑 + # cleaned_zj = "\n".join([line for line in zj.split("\n") if line.strip() != ""]) + # cleaned_zj = cleaned_zj.replace('↓', '').replace('→', '').replace('←', '') # 显式替换常见箭头 + cleaned_zj = re.sub(r'[\u2190-\u21FF]', '', cleaned_zj) # 使用正则过滤Unicode箭头区字符 + # cleaned_zj = cleaned_zj.replace('\r', '') + # 其他特殊符号处理 + cleaned_zj = cleaned_zj.translate(str.maketrans({ + '\u3000': ' ', # 全角空格 + '\u00a0': ' ', # 不间断空格 + '\u2028': '\n' # 行分隔符转普通换行 + })) + cleaned_zj = re.sub(r'[\r\n]+', ' ', cleaned_zj) + print(cleaned_zj) + find_and_replace_text(doc,'GRZJA'.strip(),cleaned_zj[:210].strip()) + find_and_replace_text(doc,'GRZJB'.strip(),cleaned_zj[210:].strip()) + + if total_count > 0: + doc.save(filename) + return f"Replaced {total_count} occurrence(s) of specified texts." + else: + return f"No occurrences of specified texts found." + except Exception as e: + return f"Failed to search and replace: {str(e)}" + +@mcp.tool() +async def delete_paragraph(filename: str, paragraph_index: int) -> str: + """Delete a paragraph from a document. + + Args: + filename: Path to the Word document + paragraph_index: Index of the paragraph to delete (0-based) + """ + if not filename.endswith('.docx'): + filename += '.docx' + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate paragraph index + if paragraph_index < 0 or paragraph_index >= len(doc.paragraphs): + return f"Invalid paragraph index. Document has {len(doc.paragraphs)} paragraphs (0-{len(doc.paragraphs)-1})." + + # Delete the paragraph (by removing its content and setting it empty) + # Note: python-docx doesn't support true paragraph deletion, this is a workaround + paragraph = doc.paragraphs[paragraph_index] + p = paragraph._p + p.getparent().remove(p) + + doc.save(filename) + return f"Paragraph at index {paragraph_index} deleted successfully." + except Exception as e: + return f"Failed to delete paragraph: {str(e)}" + +@mcp.tool() +async def create_custom_style(filename: str, style_name: str, + bold: Optional[bool] = None, italic: Optional[bool] = None, + font_size: Optional[int] = None, font_name: Optional[str] = None, + color: Optional[str] = None, base_style: Optional[str] = None) -> str: + """Create a custom style in the document. + + Args: + filename: Path to the Word document + style_name: Name for the new style + bold: Set text bold (True/False) + italic: Set text italic (True/False) + font_size: Font size in points + font_name: Font name/family + color: Text color (e.g., 'red', 'blue') + base_style: Optional existing style to base this on + """ + if not filename.endswith('.docx'): + filename += '.docx' + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Build font properties dictionary + font_properties = {} + if bold is not None: + font_properties['bold'] = bold + if italic is not None: + font_properties['italic'] = italic + if font_size is not None: + font_properties['size'] = font_size + if font_name is not None: + font_properties['name'] = font_name + if color is not None: + font_properties['color'] = color + + # Create the style + new_style = create_style( + doc, + style_name, + WD_STYLE_TYPE.PARAGRAPH, + base_style=base_style, + font_properties=font_properties + ) + + doc.save(filename) + return f"Style '{style_name}' created successfully." + except Exception as e: + return f"Failed to create style: {str(e)}" + +@mcp.tool() +async def format_table(filename: str, table_index: int, + has_header_row: Optional[bool] = None, + border_style: Optional[str] = None, + shading: Optional[List[List[str]]] = None) -> str: + """Format a table with borders, shading, and structure. + + Args: + filename: Path to the Word document + table_index: Index of the table (0-based) + has_header_row: If True, formats the first row as a header + border_style: Style for borders ('none', 'single', 'double', 'thick') + shading: 2D list of cell background colors (by row and column) + """ + if not filename.endswith('.docx'): + filename += '.docx' + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate table index + if table_index < 0 or table_index >= len(doc.tables): + return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})." + + table = doc.tables[table_index] + + # Format header row if requested + if has_header_row and table.rows: + header_row = table.rows[0] + for cell in header_row.cells: + for paragraph in cell.paragraphs: + if paragraph.runs: + for run in paragraph.runs: + run.bold = True + + # Apply border style if specified + if border_style: + val_map = { + 'none': 'nil', + 'single': 'single', + 'double': 'double', + 'thick': 'thick' + } + val = val_map.get(border_style.lower(), 'single') + + # Apply to all cells + for row in table.rows: + for cell in row.cells: + set_cell_border( + cell, + top=True, + bottom=True, + left=True, + right=True, + val=val, + color="000000" + ) + + # Apply cell shading if specified + if shading: + for i, row_colors in enumerate(shading): + if i >= len(table.rows): + break + for j, color in enumerate(row_colors): + if j >= len(table.rows[i].cells): + break + try: + # Apply shading to cell + cell = table.rows[i].cells[j] + shading_elm = parse_xml(f'') + cell._tc.get_or_add_tcPr().append(shading_elm) + except: + # Skip if color format is invalid + pass + + doc.save(filename) + return f"Table at index {table_index} formatted successfully." + except Exception as e: + return f"Failed to format table: {str(e)}" + +@mcp.tool() +async def add_page_break(filename: str) -> str: + """Add a page break to the document. + + Args: + filename: Path to the Word document + """ + if not filename.endswith('.docx'): + filename += '.docx' + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + doc.add_page_break() + doc.save(filename) + return f"Page break added to {filename}." + except Exception as e: + return f"Failed to add page break: {str(e)}" + +## sse传输 +def create_starlette_app(mcp_server: Server, *, debug: bool = False) -> Starlette: + """Create a Starlette application that can serve the provided mcp server with SSE.""" + sse = SseServerTransport("/messages/") + + async def handle_sse(request: Request) -> None: + async with sse.connect_sse( + request.scope, + request.receive, + request._send, # noqa: SLF001 + ) as (read_stream, write_stream): + await mcp_server.run( + read_stream, + write_stream, + mcp_server.create_initialization_options(), + ) + + return Starlette( + debug=debug, + routes=[ + Route("/sse", endpoint=handle_sse), + Mount("/messages/", app=sse.handle_post_message), + ], + ) + +if __name__ == "__main__": + mcp_server = mcp._mcp_server + + import argparse + + parser = argparse.ArgumentParser(description='Run MCP SSE-based server') + parser.add_argument('--host', default='0.0.0.0', help='Host to bind to') + parser.add_argument('--port', type=int, default=8020, help='Port to listen on') + args = parser.parse_args() + + # Bind SSE request handling to MCP server + starlette_app = create_starlette_app(mcp_server, debug=True) + + uvicorn.run(starlette_app, host=args.host, port=args.port) \ No newline at end of file diff --git a/guymcp/tempbz.docx b/guymcp/tempbz.docx new file mode 100644 index 0000000000000000000000000000000000000000..57e47f9567706f0ac0dd77dbd0ee87097f189884 GIT binary patch literal 19177 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 H$E*Jb{tc@> literal 0 HcmV?d00001 From 76294e64a48fcaad7b4626820a81d1f1d6c34e28 Mon Sep 17 00:00:00 2001 From: guy2015 Date: Sat, 12 Apr 2025 21:44:23 +0800 Subject: [PATCH 04/17] =?UTF-8?q?=E5=A2=9E=E5=8A=A0mcp=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E5=AF=B9=E5=8E=9F=E6=9C=89=E9=A1=B9=E7=9B=AE=E7=9A=84?= =?UTF-8?q?=E6=94=B9=E5=8A=A8=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From d61c51d0d91cacd2e60d56fd8266cdab8d12b895 Mon Sep 17 00:00:00 2001 From: guy2015 Date: Sat, 12 Apr 2025 21:45:59 +0800 Subject: [PATCH 05/17] =?UTF-8?q?=E5=A2=9E=E5=8A=A0mcp=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E5=AF=B9=E5=8E=9F=E9=A1=B9=E7=9B=AE=E7=9A=84=E6=94=B9?= =?UTF-8?q?=E5=8A=A8=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From ea258cc267d509b418601cd82f322867b972dcd5 Mon Sep 17 00:00:00 2001 From: guy2015 Date: Sat, 12 Apr 2025 21:52:10 +0800 Subject: [PATCH 06/17] =?UTF-8?q?=E5=8E=9F=E6=96=87=E4=BB=B6=E5=A4=87?= =?UTF-8?q?=E5=88=86=20Rename=20chat=5Fgpt=5Fbot.py=20to=20old=5Fchat=5Fgp?= =?UTF-8?q?t=5Fbot.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/chatgpt/{chat_gpt_bot.py => old_chat_gpt_bot.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename bot/chatgpt/{chat_gpt_bot.py => old_chat_gpt_bot.py} (100%) diff --git a/bot/chatgpt/chat_gpt_bot.py b/bot/chatgpt/old_chat_gpt_bot.py similarity index 100% rename from bot/chatgpt/chat_gpt_bot.py rename to bot/chatgpt/old_chat_gpt_bot.py From cbd97bc85682f298cc8a7c808c04f6793c3b849c Mon Sep 17 00:00:00 2001 From: guy2015 Date: Sat, 12 Apr 2025 21:53:11 +0800 Subject: [PATCH 07/17] =?UTF-8?q?=E5=8E=9F=E6=96=87=E4=BB=B6=E5=A4=87?= =?UTF-8?q?=E5=88=86=20Rename=20chat=5Fgpt=5Fsession.py=20to=20old=5Fchat?= =?UTF-8?q?=5Fgpt=5Fsession.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/chatgpt/{chat_gpt_session.py => old_chat_gpt_session.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename bot/chatgpt/{chat_gpt_session.py => old_chat_gpt_session.py} (100%) diff --git a/bot/chatgpt/chat_gpt_session.py b/bot/chatgpt/old_chat_gpt_session.py similarity index 100% rename from bot/chatgpt/chat_gpt_session.py rename to bot/chatgpt/old_chat_gpt_session.py From ef99042ebe6bc2ee4022df6dd3530bf305dca406 Mon Sep 17 00:00:00 2001 From: guy2015 Date: Sat, 12 Apr 2025 21:55:34 +0800 Subject: [PATCH 08/17] =?UTF-8?q?=E5=8A=A0=E5=85=A5mcp=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E7=9A=84chat=5Fgpt=5Fbot.py=E5=92=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/chatgpt/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 145 bytes .../__pycache__/chat_gpt_bot.cpython-311.pyc | Bin 0 -> 18527 bytes .../chat_gpt_session.cpython-311.pyc | Bin 0 -> 6003 bytes .../__pycache__/client.cpython-311.pyc | Bin 0 -> 9570 bytes .../__pycache__/client.cpython-313.pyc | Bin 0 -> 8285 bytes bot/chatgpt/chat_gpt_bot.py | 287 ++++++++++++++++++ bot/chatgpt/chat_gpt_session.py | 104 +++++++ bot/chatgpt/client.py | 177 +++++++++++ bot/chatgpt/readme_guy.md | 8 + bot/chatgpt/test_client.py | 48 +++ 11 files changed, 624 insertions(+) create mode 100644 bot/chatgpt/__init__.py create mode 100644 bot/chatgpt/__pycache__/__init__.cpython-311.pyc create mode 100644 bot/chatgpt/__pycache__/chat_gpt_bot.cpython-311.pyc create mode 100644 bot/chatgpt/__pycache__/chat_gpt_session.cpython-311.pyc create mode 100644 bot/chatgpt/__pycache__/client.cpython-311.pyc create mode 100644 bot/chatgpt/__pycache__/client.cpython-313.pyc create mode 100644 bot/chatgpt/chat_gpt_bot.py create mode 100644 bot/chatgpt/chat_gpt_session.py create mode 100644 bot/chatgpt/client.py create mode 100644 bot/chatgpt/readme_guy.md create mode 100644 bot/chatgpt/test_client.py diff --git a/bot/chatgpt/__init__.py b/bot/chatgpt/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/bot/chatgpt/__pycache__/__init__.cpython-311.pyc b/bot/chatgpt/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9984762aeb29a26327944fd600ffc5e8ea9ca595 GIT binary patch literal 145 zcmZ3^%ge<81e)i5q=V?kAOZ#$p^VRLK*n^26oz01O-8?!3`I;p{%4TnFBNyInDo-h zlC}IYR0RWMCAN~LU literal 0 HcmV?d00001 diff --git a/bot/chatgpt/__pycache__/chat_gpt_bot.cpython-311.pyc b/bot/chatgpt/__pycache__/chat_gpt_bot.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..966ea2180edc6acca252c0243a60dc30e53fd119 GIT binary patch literal 18527 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 i4>Q2p9-36j+|7A{~(BVcWT+ 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 literal 0 HcmV?d00001 diff --git a/bot/chatgpt/__pycache__/client.cpython-311.pyc b/bot/chatgpt/__pycache__/client.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f0cef8f71e4c3867f615415e478c1bb8de75cce0 GIT binary patch literal 9570 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 P$c31*Ai4pn6S4ZgfxCU% literal 0 HcmV?d00001 diff --git a/bot/chatgpt/__pycache__/client.cpython-313.pyc b/bot/chatgpt/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9875b20539f8b42f93769a1e76401ef421c1776d GIT binary patch literal 8285 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%0D;^IA;i)yK(-AH(m-S z!V z2Mwx`(!9XFqaN|`bQSY7M0AG$yS#?!=Md=UP<=a^DS2#06l(`xMPKoqai$bKFvyoUX>`O7l&UdQ2;#;zXyK|*HR76lrPLo>4jmx(3*DrSYosm-rL}^mERL8 zMM(t}x-@PH0OziB6+swTv<^*Za3F|4Sd=cV)(@k7?d(`RUK&e|5FkoWr6^%AJ-nZL zk~ZSL_+S5au!sHv7Dhwx14kibYRBXKvhU$us_!y1vLXgOB6O(G=Xj~B=OfryA7!#v zaPS}EjQHgd9;%q5k?>h2J+FlLvHU5>7`yepx zJ@z^99C-;P$nG`C`c#4g8+1jGNwEH|sN#mi!bv@c${EHdS2@V=%3x5ZP#Lo%mtskd zzY0h<;V1kMGAumq8O<*SUK)O3`1#+yae2$=P-Mt=x9QkD+VT?p2lS1a7cHMv9r(kU z@1H8b@ovl8>Lr^`wE5nNzq|PM;@!({U&`5hIopIpPvqzcrQ0RiTw69qqM01c?AYpG z;$GmkY%Ym*iL`6ih)hA^pK0b(+V%6b`EuVs(XL+z--T&iA2$U}#D>*9(StVnh>0$8 zqrVa0O(QYUO>WX8hOKpg0}6S<3ksRSa2J8wyOrEa;Fg~Hph3TRoCxaZ_mKhM_YHbz z#}Wf?Ml|YefQL)`aGXr@r(tCN9AvTqoHuZ*c(9DYT`E4Mq>>lZXG}7a1XEnU6kk?f zRBQnGiyQ~uDK9aqfWit_HmGmPJicHmvoH(zB+dkkpobH~bGTX(E;4;om1D>FA3z5_ z3&L+e2J)34K1Q~Wk>z7#`UJIng1P|y1hoS2Z^-ota(#*{zgzVM`IL_6-9 str: + # guy 新建函数用于调用MCPClient + client = MCPClient() + clean_result = 'null before call gychat_loop.' + try: + server_url = "http://127.0.0.1:8020/sse" + await client.connect_to_sse_server(server_url) + res = await client.gychat_loop(querystr) + clean_result = re.sub(r'\[.*?\]', '', res) + + finally: + await client.cleanup() + return clean_result + + def reply(self, query, context=None): + # acquire reply content + if context.type == ContextType.TEXT: + logger.info("[CHATGPT] query={}".format(query)) + + session_id = context["session_id"] + reply = None + clear_memory_commands = conf().get("clear_memory_commands", ["#清除记忆"]) + if query in clear_memory_commands: + self.sessions.clear_session(session_id) + reply = Reply(ReplyType.INFO, "记忆已清除") + elif query == "#清除所有": + self.sessions.clear_all_session() + reply = Reply(ReplyType.INFO, "所有人记忆已清除") + elif query == "#更新配置": + load_config() + reply = Reply(ReplyType.INFO, "配置已更新") + if reply: + return reply + session = self.sessions.session_query(query, session_id) + logger.debug("[CHATGPT] session query={}".format(session.messages)) + + api_key = context.get("openai_api_key") + model = context.get("gpt_model") + new_args = None + if model: + new_args = self.args.copy() + new_args["model"] = model + # if context.get('stream'): + # # reply in stream + # return self.reply_text_stream(query, new_query, session_id) + + reply_content = self.reply_text(session, api_key, args=new_args) + logger.debug( + "[CHATGPT] new_query={}, session_id={}, reply_cont={}, completion_tokens={}".format( + session.messages, + session_id, + reply_content["content"], + reply_content["completion_tokens"], + ) + ) + if reply_content["completion_tokens"] == 0 and len(reply_content["content"]) > 0: + reply = Reply(ReplyType.ERROR, reply_content["content"]) + elif reply_content["completion_tokens"] > 0: + self.sessions.session_reply(reply_content["content"], session_id, reply_content["total_tokens"]) + reply = Reply(ReplyType.TEXT, reply_content["content"]) + else: + reply = Reply(ReplyType.ERROR, reply_content["content"]) + logger.debug("[CHATGPT] reply {} used 0 tokens.".format(reply_content)) + return reply + + elif context.type == ContextType.IMAGE_CREATE: + ok, retstring = self.create_img(query, 0) + reply = None + if ok: + reply = Reply(ReplyType.IMAGE_URL, retstring) + else: + reply = Reply(ReplyType.ERROR, retstring) + return reply + else: + reply = Reply(ReplyType.ERROR, "Bot不支持处理{}类型的消息".format(context.type)) + return reply + + def reply_text(self, session: ChatGPTSession, api_key=None, args=None, retry_count=0) -> dict: + """ + call openai's ChatCompletion to get the answer + :param session: a conversation session + :param session_id: session id + :param retry_count: retry count + :return: {} + """ + try: + if conf().get("rate_limit_chatgpt") and not self.tb4chatgpt.get_token(): + raise openai.RateLimitError("RateLimitError: rate limit exceeded") + # if api_key == None, the default openai.api_key will be used + if args is None: + args = self.args + # 旧版的openai创建对话的方法因引入1.0以后,所以使用新版本的create方法 + # response = openai.ChatCompletion.create(api_key=api_key, messages=session.messages, **args) + response = openai.chat.completions.create( + model="Qwen/Qwen2.5-72B-Instruct", + # model=os.getenv("OPENAI_MODEL"), + max_tokens=1000, + messages=session.messages + ) + + # guy + guy_answer = "use get MCP answer" + user_content = next( + (msg["content"] for msg in reversed(session.messages) if msg.get("role") == "user"), + "没有找到用户消息" + ) + guy_answer = asyncio.run(self.gy_getanswer(user_content)) + logger.debug("[CHATGPT] response={}".format(response)) + # logger.info("[ChatGPT] reply={}, total_tokens={}".format(response.choices[0]['message']['content'], response["usage"]["total_tokens"])) + return { + # "total_tokens": response["usage"]["total_tokens"], 旧版本的openai的返回值 + # "completion_tokens": response["usage"]["completion_tokens"], + # "content": response.choices[0]["message"]["content"], + "total_tokens": response.usage.total_tokens, + "completion_tokens": response.usage.completion_tokens, + "content":guy_answer, + } + except Exception as e: + need_retry = retry_count < 2 + result = {"completion_tokens": 0, "content": "我现在有点累了,等会再来吧"} + if isinstance(e, openai.RateLimitError): + logger.warn("[CHATGPT] RateLimitError: {}".format(e)) + result["content"] = "提问太快啦,请休息一下再问我吧" + if need_retry: + time.sleep(20) + elif isinstance(e, openai.Timeout): + logger.warn("[CHATGPT] Timeout: {}".format(e)) + result["content"] = "我没有收到你的消息" + if need_retry: + time.sleep(5) + elif isinstance(e, openai.APIError): + logger.warn("[CHATGPT] Bad Gateway: {}".format(e)) + result["content"] = "请再问我一次" + if need_retry: + time.sleep(10) + elif isinstance(e, openai.APIConnectionError): + logger.warn("[CHATGPT] APIConnectionError: {}".format(e)) + result["content"] = "我连接不到你的网络" + if need_retry: + time.sleep(5) + else: + logger.exception("[CHATGPT] Exception: {}".format(e)) + need_retry = False + self.sessions.clear_session(session.session_id) + + if need_retry: + logger.warn("[CHATGPT] 第{}次重试".format(retry_count + 1)) + return self.reply_text(session, api_key, args, retry_count + 1) + else: + return result + + +class AzureChatGPTBot(ChatGPTBot): + def __init__(self): + super().__init__() + openai.api_type = "azure" + openai.api_version = conf().get("azure_api_version", "2023-06-01-preview") + self.args["deployment_id"] = conf().get("azure_deployment_id") + + def create_img(self, query, retry_count=0, api_key=None): + text_to_image_model = conf().get("text_to_image") + if text_to_image_model == "dall-e-2": + api_version = "2023-06-01-preview" + endpoint = conf().get("azure_openai_dalle_api_base","open_ai_api_base") + # 检查endpoint是否以/结尾 + if not endpoint.endswith("/"): + endpoint = endpoint + "/" + url = "{}openai/images/generations:submit?api-version={}".format(endpoint, api_version) + api_key = conf().get("azure_openai_dalle_api_key","open_ai_api_key") + headers = {"api-key": api_key, "Content-Type": "application/json"} + try: + body = {"prompt": query, "size": conf().get("image_create_size", "256x256"),"n": 1} + submission = requests.post(url, headers=headers, json=body) + operation_location = submission.headers['operation-location'] + status = "" + while (status != "succeeded"): + if retry_count > 3: + return False, "图片生成失败" + response = requests.get(operation_location, headers=headers) + status = response.json()['status'] + retry_count += 1 + image_url = response.json()['result']['data'][0]['url'] + return True, image_url + except Exception as e: + logger.error("create image error: {}".format(e)) + return False, "图片生成失败" + elif text_to_image_model == "dall-e-3": + api_version = conf().get("azure_api_version", "2024-02-15-preview") + endpoint = conf().get("azure_openai_dalle_api_base","open_ai_api_base") + # 检查endpoint是否以/结尾 + if not endpoint.endswith("/"): + endpoint = endpoint + "/" + url = "{}openai/deployments/{}/images/generations?api-version={}".format(endpoint, conf().get("azure_openai_dalle_deployment_id","text_to_image"),api_version) + api_key = conf().get("azure_openai_dalle_api_key","open_ai_api_key") + headers = {"api-key": api_key, "Content-Type": "application/json"} + try: + body = {"prompt": query, "size": conf().get("image_create_size", "1024x1024"), "quality": conf().get("dalle3_image_quality", "standard")} + response = requests.post(url, headers=headers, json=body) + response.raise_for_status() # 检查请求是否成功 + data = response.json() + + # 检查响应中是否包含图像 URL + if 'data' in data and len(data['data']) > 0 and 'url' in data['data'][0]: + image_url = data['data'][0]['url'] + return True, image_url + else: + error_message = "响应中没有图像 URL" + logger.error(error_message) + return False, "图片生成失败" + + except requests.exceptions.RequestException as e: + # 捕获所有请求相关的异常 + try: + error_detail = response.json().get('error', {}).get('message', str(e)) + except ValueError: + error_detail = str(e) + error_message = f"{error_detail}" + logger.error(error_message) + return False, error_message + + except Exception as e: + # 捕获所有其他异常 + error_message = f"生成图像时发生错误: {e}" + logger.error(error_message) + return False, "图片生成失败" + else: + return False, "图片生成失败,未配置text_to_image参数" diff --git a/bot/chatgpt/chat_gpt_session.py b/bot/chatgpt/chat_gpt_session.py new file mode 100644 index 000000000..b5fc6fdf8 --- /dev/null +++ b/bot/chatgpt/chat_gpt_session.py @@ -0,0 +1,104 @@ +from bot.session_manager import Session +from common.log import logger +from common import const + +""" + e.g. [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Who won the world series in 2020?"}, + {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."}, + {"role": "user", "content": "Where was it played?"} + ] +""" + + +class ChatGPTSession(Session): + def __init__(self, session_id, system_prompt=None, model="gpt-3.5-turbo"): + super().__init__(session_id, system_prompt) + self.model = model + self.reset() + + def discard_exceeding(self, max_tokens, cur_tokens=None): + precise = True + try: + cur_tokens = self.calc_tokens() + except Exception as e: + precise = False + if cur_tokens is None: + raise e + logger.debug("Exception when counting tokens precisely for query: {}".format(e)) + while cur_tokens > max_tokens: + if len(self.messages) > 2: + self.messages.pop(1) + elif len(self.messages) == 2 and self.messages[1]["role"] == "assistant": + self.messages.pop(1) + if precise: + cur_tokens = self.calc_tokens() + else: + cur_tokens = cur_tokens - max_tokens + break + elif len(self.messages) == 2 and self.messages[1]["role"] == "user": + logger.warn("user message exceed max_tokens. total_tokens={}".format(cur_tokens)) + break + else: + logger.debug("max_tokens={}, total_tokens={}, len(messages)={}".format(max_tokens, cur_tokens, len(self.messages))) + break + if precise: + cur_tokens = self.calc_tokens() + else: + cur_tokens = cur_tokens - max_tokens + return cur_tokens + + def calc_tokens(self): + return num_tokens_from_messages(self.messages, self.model) + + +# refer to https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb +def num_tokens_from_messages(messages, model): + """Returns the number of tokens used by a list of messages.""" + + if model in ["wenxin", "xunfei"] or model.startswith(const.GEMINI): + return num_tokens_by_character(messages) + + import tiktoken + + if model in ["gpt-3.5-turbo-0301", "gpt-35-turbo", "gpt-3.5-turbo-1106", "moonshot", const.LINKAI_35]: + return num_tokens_from_messages(messages, model="gpt-3.5-turbo") + elif model in ["gpt-4-0314", "gpt-4-0613", "gpt-4-32k", "gpt-4-32k-0613", "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613", "gpt-35-turbo-16k", "gpt-4-turbo-preview", + "gpt-4-1106-preview", const.GPT4_TURBO_PREVIEW, const.GPT4_VISION_PREVIEW, const.GPT4_TURBO_01_25, + const.GPT_4o, const.GPT_4O_0806, const.GPT_4o_MINI, const.LINKAI_4o, const.LINKAI_4_TURBO]: + return num_tokens_from_messages(messages, model="gpt-4") + elif model.startswith("claude-3"): + return num_tokens_from_messages(messages, model="gpt-3.5-turbo") + try: + encoding = tiktoken.encoding_for_model(model) + except KeyError: + logger.debug("Warning: model not found. Using cl100k_base encoding.") + encoding = tiktoken.get_encoding("cl100k_base") + if model == "gpt-3.5-turbo": + tokens_per_message = 4 # every message follows <|start|>{role/name}\n{content}<|end|>\n + tokens_per_name = -1 # if there's a name, the role is omitted + elif model == "gpt-4": + tokens_per_message = 3 + tokens_per_name = 1 + else: + logger.debug(f"num_tokens_from_messages() is not implemented for model {model}. Returning num tokens assuming gpt-3.5-turbo.") + return num_tokens_from_messages(messages, model="gpt-3.5-turbo") + num_tokens = 0 + for message in messages: + num_tokens += tokens_per_message + for key, value in message.items(): + num_tokens += len(encoding.encode(value)) + if key == "name": + num_tokens += tokens_per_name + num_tokens += 3 # every reply is primed with <|start|>assistant<|message|> + return num_tokens + + +def num_tokens_by_character(messages): + """Returns the number of tokens used by a list of messages.""" + tokens = 0 + for msg in messages: + tokens += len(msg["content"]) + return tokens diff --git a/bot/chatgpt/client.py b/bot/chatgpt/client.py new file mode 100644 index 000000000..826c71289 --- /dev/null +++ b/bot/chatgpt/client.py @@ -0,0 +1,177 @@ +# cd ../guymcp 运行uv run server.py --host 127.0.0.1 --port 8020 启动MCP服务器 +import asyncio +import json +import os +from typing import Optional +from contextlib import AsyncExitStack +import time +from mcp import ClientSession +from mcp.client.sse import sse_client + +from openai import AsyncOpenAI +from dotenv import load_dotenv + +load_dotenv() # load environment variables from .env + +class MCPClient: + def __init__(self): + # Initialize session and client objects + self.session: Optional[ClientSession] = None + self.exit_stack = AsyncExitStack() + # self.openai = AsyncOpenAI(api_key="sk-ausgzyjuyhyuaaizdxtzqltuimudowdrxwokgjrcgmebnwnm", base_url="https://api.siliconflow.cn/v1") + self.openai = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL")) + + async def connect_to_sse_server(self, server_url: str): + """Connect to an MCP server running with SSE transport""" + # Store the context managers so they stay alive + self._streams_context = sse_client(url=server_url) + streams = await self._streams_context.__aenter__() + + self._session_context = ClientSession(*streams) + self.session: ClientSession = await self._session_context.__aenter__() + + # Initialize + await self.session.initialize() + + # List available tools to verify connection + print("Initialized SSE client...") + print("Listing tools...") + response = await self.session.list_tools() + tools = response.tools + print("\nConnected to server with tools:", [tool.name for tool in tools]) + + async def cleanup(self): + """Properly clean up the session and streams""" + if self._session_context: + await self._session_context.__aexit__(None, None, None) + if self._streams_context: + await self._streams_context.__aexit__(None, None, None) + + async def process_query(self, query: str) -> str: + """Process a query using OpenAI API and available tools""" + messages = [ + { + "role": "user", + "content": query + } + ] + + response = await self.session.list_tools() + available_tools = [{ + "type": "function", + "function": { + "name": tool.name, + "description": tool.description, + "parameters": tool.inputSchema + } + } for tool in response.tools] + + # Initial OpenAI API call + completion = await self.openai.chat.completions.create( + model="Qwen/Qwen2.5-72B-Instruct", + # model=os.getenv("OPENAI_MODEL"), + max_tokens=1000, + messages=messages, + tools=available_tools + ) + + # Process response and handle tool calls + tool_results = [] + final_text = [] + + assistant_message = completion.choices[0].message + + if assistant_message.tool_calls: + for tool_call in assistant_message.tool_calls: + tool_name = tool_call.function.name + tool_args = json.loads(tool_call.function.arguments) + + # Execute tool call + result = await self.session.call_tool(tool_name, tool_args) + tool_results.append({"call": tool_name, "result": result}) + final_text.append(f"[Calling tool {tool_name} with args {tool_args}]") + + # Continue conversation with tool results + messages.extend([ + { + "role": "assistant", + "content": None, + "tool_calls": [tool_call] + }, + { + "role": "tool", + "tool_call_id": tool_call.id, + "content": result.content[0].text + } + ]) + + print(f"Tool {tool_name} returned: {result.content[0].text}") + print("messages", messages) + # Get next response from OpenAI + completion = await self.openai.chat.completions.create( + model=os.getenv("OPENAI_MODEL"), + max_tokens=1000, + messages=messages, + ) + if isinstance(completion.choices[0].message.content, (dict, list)): + final_text.append(str(completion.choices[0].message.content)) + else: + final_text.append(completion.choices[0].message.content) + else: + if isinstance(assistant_message.content, (dict, list)): + final_text.append(str(assistant_message.content)) + else: + final_text.append(assistant_message.content) + + return "\n".join(final_text) + + async def chat_loop(self): + """Run an interactive chat loop""" + print("\nMCP Client Started!") + print("Type your queries or 'quit' to exit.") + + while True: + try: + query = input("\nQuery: ").strip() + + if query.lower() == 'quit': + break + + response = await self.process_query(query) + print("\n" + response) + + except Exception as e: + print(f"\nError: {str(e)}") + async def gychat_loop(self,querystr) -> str: + """Run an interactive chat loop""" + print("\nMCP Client Started!") + print("Type your queries or 'quit' to exit.") + response = "响应初始化..." + try: + # query = input("\nQuery: ").strip() + query = querystr + if query.lower() == 'quit': + return + + response = await self.process_query(query) + print("\n" + response) + + except Exception as e: + print(f"\nError: {str(e)}") + finally: + return response +async def main(): + if len(sys.argv) < 2: + print("Usage: uv run client.py ") + sys.exit(1) + + client = MCPClient() + try: + await client.connect_to_sse_server(server_url=sys.argv[1]) + await client.chat_loop() + finally: + await client.cleanup() + +if __name__ == "__main__": + import sys + asyncio.run(main()) \ No newline at end of file diff --git a/bot/chatgpt/readme_guy.md b/bot/chatgpt/readme_guy.md new file mode 100644 index 000000000..676750df9 --- /dev/null +++ b/bot/chatgpt/readme_guy.md @@ -0,0 +1,8 @@ +1.仅修改在chat_gpt_bot.py中,引入clent.py定义的MCPClient类,用于加入mcp服务作为中介, + 在chat_gpt_bot.py自定义了gy_getanswer(self,querystr) + +2.本目录下的client.py 用于方便测试,是上两级guymcp目录下的client.py的副本,两者完全一致 + 对应导入为from bot.chatgpt.client import MCPClient + +3.test_client.py 用于测试client.py + diff --git a/bot/chatgpt/test_client.py b/bot/chatgpt/test_client.py new file mode 100644 index 000000000..2c32106be --- /dev/null +++ b/bot/chatgpt/test_client.py @@ -0,0 +1,48 @@ +# cd ../../guymcp 运行uv run server.py --host 127.0.0.1 --port 8020 启动MCP服务器 +import time + +import openai +# import openai.error +import requests +# # from common import const +# from bot.bot import Bot +# from bot.chatgpt.chat_gpt_session import ChatGPTSession +# from bot.openai.open_ai_image import OpenAIImage +# from bot.session_manager import SessionManager +# from bridge.context import ContextType +# from bridge.reply import Reply, ReplyType +# from common.log import logger +# from common.token_bucket import TokenBucket +# from config import conf, load_config +# from bot.baidu.baidu_wenxin_session import BaiduWenxinSession + +import re +# from bot.chatgpt.client import MCPClient +from client import MCPClient +import asyncio + + +async def gy_getanswer(querystr) -> str: +# guy + client = MCPClient() + print("=======>befor call gychat_loop.") + clean_result = 'null before call gychat_loop.' + try: + # server_url = "http://0.0.0.0:8020/sse" + server_url = "http://127.0.0.1:8020/sse" + await client.connect_to_sse_server(server_url) + # await client.chat_loop() + res = await client.gychat_loop(querystr) + print(res) + clean_result = re.sub(r'\[.*?\]', '', res) + + finally: + print(f"[****SUCESS call gychat_loop****]: {clean_result}") + await client.cleanup() + print("<=======after gychat_loop.") + print("<=======after cleanup.") + print(clean_result) + return clean_result + +guy_answer = asyncio.run(gy_getanswer('查询张三的电话号码')) +print("guy_answer:",guy_answer) \ No newline at end of file From f10931d5641e7bb51b7fe8a5418b62f2cee028ba Mon Sep 17 00:00:00 2001 From: guy2015 Date: Sat, 12 Apr 2025 21:56:48 +0800 Subject: [PATCH 09/17] Delete GUY.md --- GUY.md | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 GUY.md diff --git a/GUY.md b/GUY.md deleted file mode 100644 index 52aa6c2f8..000000000 --- a/GUY.md +++ /dev/null @@ -1,3 +0,0 @@ -1.增加了guymcp目录,下含mcp服务端和客户端,以及mcp的配置文件。 - -2 修改原项目 bot.chatgpt 下chat_gpt_bot.py文件,加入mcp客户端的应答。 From 8ab47a8b40d7cf4017b3724f8b89acd9903133fe Mon Sep 17 00:00:00 2001 From: guy2015 Date: Sat, 12 Apr 2025 21:59:01 +0800 Subject: [PATCH 10/17] =?UTF-8?q?=E5=A2=9E=E5=8A=A0MCP=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E5=AF=B9=E5=8E=9F=E9=A1=B9=E7=9B=AE=E6=94=B9=E5=8A=A8?= =?UTF-8?q?=E7=9A=84=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GUYMCP.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 GUYMCP.md diff --git a/GUYMCP.md b/GUYMCP.md new file mode 100644 index 000000000..52aa6c2f8 --- /dev/null +++ b/GUYMCP.md @@ -0,0 +1,3 @@ +1.增加了guymcp目录,下含mcp服务端和客户端,以及mcp的配置文件。 + +2 修改原项目 bot.chatgpt 下chat_gpt_bot.py文件,加入mcp客户端的应答。 From 5c7415c1a69753bb34d6bc98e97df3dfbd646dcd Mon Sep 17 00:00:00 2001 From: guy2015 Date: Sat, 12 Apr 2025 22:07:28 +0800 Subject: [PATCH 11/17] =?UTF-8?q?api=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E9=9C=80=E8=87=AA=E8=A1=8C=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/chatgpt/.env | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 bot/chatgpt/.env diff --git a/bot/chatgpt/.env b/bot/chatgpt/.env new file mode 100644 index 000000000..1812fe694 --- /dev/null +++ b/bot/chatgpt/.env @@ -0,0 +1,10 @@ +# Server Configuration +MCP_PORT=8020 +SERPER_API_KEY=xxx + +# Client Configuration +MCP_SERVER_URL=http://localhost:8020 + +OPENAI_API_KEY=sk-ausgzyjuyhyuaaizdxtzqltuimudowdrxwokgjrcgmebnwnm +OPENAI_BASE_URL=https://api.siliconflow.cn/v1 +OPENAI_MODEL=Qwen/Qwen2.5-72B-Instruct \ No newline at end of file From 8d477a6ea512324042d9fbebb4049d0083f3f13c Mon Sep 17 00:00:00 2001 From: guy2015 Date: Sat, 12 Apr 2025 22:08:50 +0800 Subject: [PATCH 12/17] Update .env --- bot/chatgpt/.env | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/chatgpt/.env b/bot/chatgpt/.env index 1812fe694..835cdd1f3 100644 --- a/bot/chatgpt/.env +++ b/bot/chatgpt/.env @@ -5,6 +5,6 @@ SERPER_API_KEY=xxx # Client Configuration MCP_SERVER_URL=http://localhost:8020 -OPENAI_API_KEY=sk-ausgzyjuyhyuaaizdxtzqltuimudowdrxwokgjrcgmebnwnm -OPENAI_BASE_URL=https://api.siliconflow.cn/v1 -OPENAI_MODEL=Qwen/Qwen2.5-72B-Instruct \ No newline at end of file +OPENAI_API_KEY= +OPENAI_BASE_URL= +OPENAI_MODEL= From 299138b60d99dbc9680617051b43b24f7538f31a Mon Sep 17 00:00:00 2001 From: guy2015 Date: Sat, 12 Apr 2025 22:09:52 +0800 Subject: [PATCH 13/17] Update client.py --- bot/chatgpt/client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/chatgpt/client.py b/bot/chatgpt/client.py index 826c71289..011162ad1 100644 --- a/bot/chatgpt/client.py +++ b/bot/chatgpt/client.py @@ -18,7 +18,6 @@ def __init__(self): # Initialize session and client objects self.session: Optional[ClientSession] = None self.exit_stack = AsyncExitStack() - # self.openai = AsyncOpenAI(api_key="sk-ausgzyjuyhyuaaizdxtzqltuimudowdrxwokgjrcgmebnwnm", base_url="https://api.siliconflow.cn/v1") self.openai = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL")) async def connect_to_sse_server(self, server_url: str): @@ -174,4 +173,4 @@ async def main(): if __name__ == "__main__": import sys - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) From 0eb266d1e1d2d79a24423cbf719569795f910b09 Mon Sep 17 00:00:00 2001 From: guy2015 Date: Sat, 12 Apr 2025 22:11:34 +0800 Subject: [PATCH 14/17] =?UTF-8?q?mcp=E6=9C=8D=E5=8A=A1=E5=92=8C=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E5=8F=8A=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- guymcp/.env | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 guymcp/.env diff --git a/guymcp/.env b/guymcp/.env new file mode 100644 index 000000000..1812fe694 --- /dev/null +++ b/guymcp/.env @@ -0,0 +1,10 @@ +# Server Configuration +MCP_PORT=8020 +SERPER_API_KEY=xxx + +# Client Configuration +MCP_SERVER_URL=http://localhost:8020 + +OPENAI_API_KEY=sk-ausgzyjuyhyuaaizdxtzqltuimudowdrxwokgjrcgmebnwnm +OPENAI_BASE_URL=https://api.siliconflow.cn/v1 +OPENAI_MODEL=Qwen/Qwen2.5-72B-Instruct \ No newline at end of file From 6c7f38b8ddc0482a59b161bbaed00b7dc56bb6b6 Mon Sep 17 00:00:00 2001 From: guy2015 Date: Sat, 12 Apr 2025 22:12:27 +0800 Subject: [PATCH 15/17] =?UTF-8?q?mcp=E6=9C=8D=E5=8A=A1=E5=92=8C=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E5=8F=8A=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- guymcp/.env | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/guymcp/.env b/guymcp/.env index 1812fe694..835cdd1f3 100644 --- a/guymcp/.env +++ b/guymcp/.env @@ -5,6 +5,6 @@ SERPER_API_KEY=xxx # Client Configuration MCP_SERVER_URL=http://localhost:8020 -OPENAI_API_KEY=sk-ausgzyjuyhyuaaizdxtzqltuimudowdrxwokgjrcgmebnwnm -OPENAI_BASE_URL=https://api.siliconflow.cn/v1 -OPENAI_MODEL=Qwen/Qwen2.5-72B-Instruct \ No newline at end of file +OPENAI_API_KEY= +OPENAI_BASE_URL= +OPENAI_MODEL= From 6cc0d6d401e778442fe557b26766c306d4d6580f Mon Sep 17 00:00:00 2001 From: guy2015 Date: Sat, 12 Apr 2025 22:13:06 +0800 Subject: [PATCH 16/17] =?UTF-8?q?mcp=E6=9C=8D=E5=8A=A1=E5=92=8C=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E5=8F=8A=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- guymcp/client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/guymcp/client.py b/guymcp/client.py index f83c6ee10..fdab09073 100644 --- a/guymcp/client.py +++ b/guymcp/client.py @@ -17,7 +17,6 @@ def __init__(self): # Initialize session and client objects self.session: Optional[ClientSession] = None self.exit_stack = AsyncExitStack() - # self.openai = AsyncOpenAI(api_key="sk-ausgzyjuyhyuaaizdxtzqltuimudowdrxwokgjrcgmebnwnm", base_url="https://api.siliconflow.cn/v1") self.openai = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL")) async def connect_to_sse_server(self, server_url: str): @@ -173,4 +172,4 @@ async def main(): if __name__ == "__main__": import sys - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) From 42af0132e378ebc55026c59887da9c754c27b4db Mon Sep 17 00:00:00 2001 From: guy2015 Date: Sat, 12 Apr 2025 22:14:18 +0800 Subject: [PATCH 17/17] =?UTF-8?q?=E5=8A=A0=E5=85=A5mcp=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E7=9A=84chat=5Fgpt=5Fbot.py=E5=92=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/chatgpt/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/chatgpt/client.py b/bot/chatgpt/client.py index 011162ad1..f5a2e9372 100644 --- a/bot/chatgpt/client.py +++ b/bot/chatgpt/client.py @@ -1,4 +1,4 @@ -# cd ../guymcp 运行uv run server.py --host 127.0.0.1 --port 8020 启动MCP服务器 +# cd ../guymcp 运行uv run server.py --host 127.0.0.1 --port 8020 启动MCP服务器 import asyncio import json import os