gomyck-tools 1.3.3__py3-none-any.whl → 1.3.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ctools/ai/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: UTF-8 -*-
3
+ __author__ = 'haoyang'
4
+ __date__ = '2025/5/22 15:56'
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: UTF-8 -*-
3
+ __author__ = 'haoyang'
4
+ __date__ = '2025/5/16 16:42'
5
+
6
+ import json
7
+ import os
8
+ from typing import Any
9
+
10
+ from dotenv.main import DotEnv
11
+
12
+
13
+ class Configuration:
14
+ """Manages configuration and environment variables for the MCP client."""
15
+
16
+ def __init__(self, dotenv_path: str=".env") -> None:
17
+ """Initialize configuration with environment variables."""
18
+ if not os.path.exists(dotenv_path): raise FileNotFoundError(f"Could not find .env file at {dotenv_path}")
19
+ self.env = DotEnv(dotenv_path=dotenv_path)
20
+
21
+ def get_env(self, key: str) -> str:
22
+ value = self.env.get(key)
23
+ return value if value else os.getenv(key)
24
+
25
+ def get_llm_api_key(self) -> str:
26
+ api_key = self.get_env("LLM_API_KEY")
27
+ if not api_key: raise ValueError("LLM_API_KEY not found in environment variables")
28
+ return api_key
29
+
30
+ def get_mcp_server_config(self) -> dict[str, Any]:
31
+ with open(self.get_env("MCP_CONFIG_PATH"), "r") as f:
32
+ return json.load(f)
33
+
ctools/ai/llm_chat.py ADDED
@@ -0,0 +1,174 @@
1
+ import enum
2
+ import re
3
+
4
+ from ctools import sys_log
5
+ from ctools.ai.llm_client import LLMClient
6
+ from ctools.ai.mcp.mcp_client import MCPClient, res_has_img, is_image_content, get_tools_prompt, mcp_tool_call
7
+ from ctools.ai.tools.xml_extract import extract_all_xml_blocks
8
+
9
+ log = sys_log.flog
10
+
11
+ continue_prompt_default = """
12
+ 1.请继续处理尚未完成的内容,跳过所有已处理完成的部分.
13
+ 2.工具调用时,请参考上一次工具调用的参数,仅对偏移量相关的参数进行调整,以接续上一次处理的进度.
14
+ 3.如果你认为所有数据都处理完毕, 请输出标记:{end_flag}.
15
+ """
16
+
17
+ class ROLE:
18
+ ASSISTANT = "assistant"
19
+ USER = "user"
20
+ SYSTEM = "system"
21
+
22
+ def get_message_json(role_type: ROLE, content):
23
+ return {"role": role_type, "content": content}
24
+
25
+ def remove_think_blocks(text: str) -> str:
26
+ cleaned_text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
27
+ return cleaned_text.strip()
28
+
29
+ class ChatSession:
30
+
31
+ def __init__(self, prompts: str, llm_client: LLMClient, max_tools_call: int = 10, mcp_clients: list[MCPClient] = None,
32
+ auto_complete: bool = False, end_flag: str = "EOF", continue_prompt: str = continue_prompt_default) -> None:
33
+ """
34
+ 初始化聊天
35
+ :param prompts: 提示词
36
+ :param llm_client: llm 客户端
37
+ :param max_tools_call: 最大单次工具调用次数
38
+ :param mcp_clients: mcp_clients
39
+ :param auto_complete: 是否自动完成
40
+ :param end_flag: 结束标记
41
+ :param continue_prompt: 自动完成时的继续提示
42
+ """
43
+ self.mcp_clients: list[MCPClient] = mcp_clients
44
+ self.llm_client: LLMClient = llm_client
45
+ self.prompts: str = prompts
46
+ self.max_tools_call = max_tools_call
47
+ self.auto_complete = auto_complete
48
+ self.end_flag = end_flag
49
+ self.continue_prompt = continue_prompt.format(end_flag=self.end_flag)
50
+
51
+ self.current_message = {}
52
+ self.full_messages = []
53
+
54
+ async def init_prompts(self, user_system_prompt):
55
+ if self.mcp_clients:
56
+ if user_system_prompt:
57
+ mcp_tools_prompt = await get_tools_prompt(self.mcp_clients, user_system_prompt)
58
+ elif self.prompts:
59
+ mcp_tools_prompt = await get_tools_prompt(self.mcp_clients, self.prompts)
60
+ else:
61
+ mcp_tools_prompt = await get_tools_prompt(self.mcp_clients, "")
62
+ self.full_messages.append(get_message_json(ROLE.SYSTEM, mcp_tools_prompt))
63
+ #log.info(mcp_tools_prompt)
64
+ else:
65
+ if user_system_prompt:
66
+ self.full_messages.append(get_message_json(ROLE.SYSTEM, user_system_prompt))
67
+ elif self.prompts:
68
+ self.full_messages.append(get_message_json(ROLE.SYSTEM, self.prompts))
69
+
70
+ async def chat(self, user_input: [str], get_call_id: callable(str) = lambda: "None", get_event_msg_func: callable(str) = None, get_full_msg_func: callable(str) = None):
71
+ """
72
+ 对话
73
+ Parameters
74
+ ----------
75
+ user_input 用户输入 [{"role": "user", "content": "hello"}]
76
+ get_call_id 本次对话 ID func
77
+ get_event_msg_func(get_call_id(), role, msg) 获取实时回复(流式回答)
78
+ get_full_msg_func(get_call_id(), is_final, msg) 获取完整的回答列表
79
+ -------
80
+ """
81
+ # 获取 prompt
82
+ user_system_prompt = user_input[0]["content"] if user_input[0]["role"] == "system" else ""
83
+ user_input = user_input[1:] if user_input[0]["role"] == "system" else user_input
84
+ await self.init_prompts(user_system_prompt)
85
+ try:
86
+ self.full_messages.extend(user_input)
87
+ last_user_input = next((item["content"] for item in reversed(user_input) if item.get("role") == "user"), None)
88
+ current_process_index = 0
89
+ max_tools_call = self.max_tools_call
90
+ final_resp = False
91
+ while (current_process_index < max_tools_call and not final_resp) or (self.auto_complete and current_process_index < 100):
92
+ log.info("\n正在生成回答: %s", self.full_messages)
93
+ res = []
94
+ async for chunk in self.llm_client.model_completion(self.full_messages):
95
+ res.append(chunk)
96
+ await self.process_chunk_message(chunk, get_call_id, get_event_msg_func)
97
+ llm_response = "".join(res)
98
+ log.info("\n收到回答: %s", llm_response)
99
+ no_think_llm_response = remove_think_blocks(llm_response) # 处理掉 think, 然后再判断 EOF, 避免 think 里出现 EOF
100
+ if self.end_flag in no_think_llm_response:
101
+ self.full_messages.append(get_message_json(ROLE.ASSISTANT, llm_response.replace(self.end_flag, ""))) # 去掉 EOF
102
+ current_process_index = 999
103
+ final_resp = True
104
+ await self.process_full_message(final_resp, get_call_id, get_full_msg_func)
105
+ else:
106
+ xml_blocks = extract_all_xml_blocks(llm_response)
107
+ if xml_blocks:
108
+ for xml_block in xml_blocks:
109
+ tool_call_result = await mcp_tool_call(self.mcp_clients, xml_block)
110
+ log.info("\nMCP调用结果: %s", tool_call_result)
111
+ current_process_index += 1
112
+ if tool_call_result == xml_block:
113
+ self.full_messages.append(get_message_json(ROLE.USER, "工具调用出现错误, 请重试."))
114
+ elif current_process_index == max_tools_call - 1:
115
+ await self.add_tool_call_res_2_message(last_user_input, tool_call_result)
116
+ self.full_messages.append(get_message_json(ROLE.USER, "调用次数已达上限, 请直接回答.")) # 不能调换顺序
117
+ else:
118
+ self.full_messages.append(get_message_json(ROLE.ASSISTANT, llm_response)) # 不能调换顺序
119
+ await self.add_tool_call_res_2_message(last_user_input, tool_call_result)
120
+ await self.process_tool_call_message(get_call_id, get_event_msg_func, tool_call_result)
121
+ final_resp = False
122
+ else:
123
+ self.full_messages.append(get_message_json(ROLE.ASSISTANT, llm_response))
124
+ if self.auto_complete: self.full_messages.append(get_message_json(ROLE.USER, self.continue_prompt))
125
+ final_resp = True
126
+ await self.process_full_message(final_resp, get_call_id, get_full_msg_func)
127
+ except Exception as e:
128
+ log.exception(e)
129
+ error_msg = '系统出现错误, 请重试~ {}'.format(e)
130
+ self.full_messages.append(get_message_json(ROLE.ASSISTANT, error_msg))
131
+ await self.process_error_message(error_msg, get_call_id, get_event_msg_func, get_full_msg_func)
132
+ finally:
133
+ return self.current_message
134
+
135
+ async def process_error_message(self, error_msg, get_call_id, get_event_msg_func, get_full_msg_func):
136
+ # 最终结果通知前端+实时通知都要有
137
+ self.current_message = error_msg
138
+ if get_event_msg_func: await get_event_msg_func(get_call_id(), ROLE.ASSISTANT, self.current_message)
139
+ if get_full_msg_func: await get_full_msg_func(get_call_id(), True, self.full_messages)
140
+
141
+ async def process_chunk_message(self, chunk, get_call_id, get_event_msg_func):
142
+ # 实时通知前端
143
+ self.current_message = chunk
144
+ if get_event_msg_func: await get_event_msg_func(get_call_id(), ROLE.ASSISTANT, self.current_message)
145
+
146
+ async def process_tool_call_message(self, get_call_id, get_event_msg_func, tool_call_result):
147
+ # 实时通知前端(工具调用特殊通知一次) 如果是图片结果, 就是 user 消息(必须是 user, 否则 api 报错), 否则是 system(现在统一都改成 user 了, 看看后面有没有改回 system 的必要)
148
+ self.current_message = tool_call_result["result"] if res_has_img(tool_call_result) else tool_call_result
149
+ if get_event_msg_func: await get_event_msg_func(get_call_id(), ROLE.USER, self.current_message)
150
+
151
+ async def process_full_message(self, final_resp, get_call_id, get_full_msg_func):
152
+ if get_full_msg_func: await get_full_msg_func(get_call_id(), final_resp, self.full_messages)
153
+
154
+ async def add_tool_call_res_2_message(self, last_user_input, tool_call_result: dict):
155
+ if type(tool_call_result) != dict: return
156
+ response:[] = tool_call_result.get("result")
157
+ image_content = []
158
+ for rep in response:
159
+ if not is_image_content(rep):
160
+ self.full_messages.append(get_message_json(ROLE.USER, str(rep)))
161
+ else:
162
+ image_content.append({
163
+ "type": "image_url",
164
+ "image_url": {
165
+ "url": f'data:{rep["mime_type"]};base64,{rep["data"]}'
166
+ }
167
+ })
168
+ if image_content:
169
+ image_content.append({
170
+ "type": "text",
171
+ "text": last_user_input
172
+ })
173
+ self.full_messages.append(get_message_json(ROLE.USER, image_content))
174
+
@@ -0,0 +1,120 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+
5
+ import httpx
6
+
7
+ from ctools import sys_log, cjson
8
+
9
+ logging.getLogger("httpcore").setLevel(logging.WARNING)
10
+ logging.getLogger("httpx").setLevel(logging.WARNING)
11
+ logging.getLogger("mcp.client.sse").setLevel(logging.WARNING)
12
+
13
+ log = sys_log.flog
14
+
15
+ def process_SSE(line):
16
+ if not line: return None
17
+ if line.startswith("data: "):
18
+ data = line[6:]
19
+ if data == "[DONE]":
20
+ return "DONE"
21
+ return data
22
+
23
+ class LLMClient:
24
+ """Manages communication with the LLM provider."""
25
+
26
+ def __init__(self, api_key: str=os.getenv("LLM_API_KEY"), llm_url: str="https://api.siliconflow.cn/v1/", model_name: str="Qwen/Qwen3-235B-A22B", temperature: float=1, stream: bool=True, thinking: bool=True, thinking_budget: int=4096, max_tokens: int=8192) -> None:
27
+ self.api_key = api_key
28
+ self.llm_url = llm_url
29
+ self.model_name = model_name
30
+ self.temperature = temperature
31
+ self.stream = stream
32
+ self.thinking = thinking
33
+ self.thinking_budget = thinking_budget
34
+ self.max_tokens = max_tokens
35
+
36
+ async def model_completion(self, messages: list[dict[str, str]]):
37
+ self.no_think_compatible(messages)
38
+ url = self.llm_url
39
+ headers = {
40
+ "Content-Type": "application/json",
41
+ "Authorization": f"Bearer {self.api_key}",
42
+ }
43
+ payload = {
44
+ "messages": messages,
45
+ "model": self.model_name,
46
+ "temperature": self.temperature,
47
+ "max_tokens": self.max_tokens,
48
+ "top_p": 0.5,
49
+ "top_k": 50,
50
+ "frequency_penalty": 0.7,
51
+ "stream": self.stream,
52
+ "enable_thinking": self.thinking,
53
+ "thinking_budget": self.thinking_budget
54
+ }
55
+ try:
56
+ req_url = "chat/completions"
57
+ if self.stream:
58
+ async with httpx.AsyncClient(timeout=None, base_url=url) as client:
59
+ async with client.stream("POST", req_url, headers=headers, json=payload) as response:
60
+ response.raise_for_status()
61
+ # 兼容 DS QWEN 的思维链
62
+ start_think: bool = False
63
+ end_think: bool = False
64
+ start_token: str = "<think>"
65
+ end_token: str = "</think>"
66
+ # 兼容 DS QWEN 的思维链
67
+ async for line in response.aiter_lines():
68
+ data = process_SSE(line)
69
+ if not data: continue
70
+ if data == "DONE":
71
+ continue
72
+ choice = cjson.loads(data)["choices"][0]
73
+ if "message" in choice:
74
+ content = choice["message"]["content"]
75
+ else:
76
+ content = choice["delta"].get("content", "")
77
+ # 兼容 DS QWEN 的思维链
78
+ reasoning_content = choice["delta"].get("reasoning_content", "")
79
+ if not start_think and not content and reasoning_content:
80
+ content = f"{start_token}{reasoning_content}"
81
+ start_think = True
82
+ if not end_think and start_think and not reasoning_content:
83
+ content = f"{end_token}{content}"
84
+ end_think = True
85
+ if not content: content = reasoning_content
86
+ if not content: continue
87
+ # 兼容 DS QWEN 的思维链
88
+ yield content
89
+ else:
90
+ async with httpx.AsyncClient(timeout=None, base_url=url) as client:
91
+ response = await client.post(req_url, headers=headers, json=payload)
92
+ response.raise_for_status()
93
+ content = response.json()["choices"][0]["message"]["content"]
94
+ yield content
95
+ except httpx.RequestError as e:
96
+ error_message = f"Error getting LLM response: {str(e)}"
97
+ log.error(error_message)
98
+ if isinstance(e, httpx.HTTPStatusError):
99
+ status_code = e.response.status_code
100
+ log.error(f"Status code: {status_code}")
101
+ log.error(f"Response details: {e.response.text}")
102
+ yield f"I encountered an error: {error_message}. Please try again or rephrase your request."
103
+
104
+ def no_think_compatible(self, messages):
105
+ if not self.thinking and "qwen3" in self.model_name:
106
+ for msg in messages:
107
+ if (msg.get("role") == "user" or msg.get("role") == "system") and "/no_think" not in msg.get("content", ""):
108
+ msg["content"] += " /no_think"
109
+
110
+ # if __name__ == '__main__':
111
+ # from env_config import Configuration
112
+ #
113
+ # config = Configuration()
114
+ # # llm = LLMClient(config.get_llm_api_key(), llm_url="http://192.168.3.73:8000/v1/", stream=True, model_name="deepseek-r1:7b", thinking=False, verbose=True)
115
+ # llm = LLMClient(config.get_llm_api_key(), stream=True, model_name="Qwen/Qwen3-32B", thinking=False, verbose=True)
116
+ # res = []
117
+ # for chunk in llm.get_response([{"role": "user", "content": "写一个大概三百字的开心故事"}]):
118
+ # res.append(chunk)
119
+ # print("".join(res))
120
+
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: UTF-8 -*-
3
+ __author__ = 'haoyang'
4
+ __date__ = '2025/5/22 15:56'
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: UTF-8 -*-
3
+ __author__ = 'haoyang'
4
+ __date__ = '2025/5/16 16:49'
5
+
6
+ import asyncio
7
+ import json
8
+ import shutil
9
+ from contextlib import AsyncExitStack, asynccontextmanager
10
+ from typing import Any
11
+
12
+ from mcp import ClientSession, StdioServerParameters
13
+ from mcp.client.sse import sse_client
14
+ from mcp.client.stdio import stdio_client
15
+ from mcp.client.streamable_http import streamablehttp_client
16
+ from mcp.types import CallToolResult, TextContent, ImageContent
17
+
18
+ from ctools import sys_log
19
+ from ctools.ai.tools.tool_use_xml_parse import parse_tool_use
20
+
21
+ log = sys_log.flog
22
+
23
+ sys_prompt_4_mcp = """
24
+ 1. In this environment you have access to a set of tools you can use to answer the user's question.
25
+ 2. You can use one tool per message, and will receive the result of that tool use in the user's response.
26
+ 3. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
27
+ 4. Before solving the task, break it down into clear, logical steps. List and number the steps first. Then, execute them one by one, There is no need to explain each step as you go. Do not skip any steps. Wait for confirmation before proceeding to the next step, if needed.
28
+
29
+ ## Tool Use Formatting
30
+
31
+ Tool use is formatted using XML-style tags. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags. Here's the structure:
32
+ <tool_use>
33
+ <name>{{tool_name}}</name>
34
+ <arguments>{{json_arguments}}</arguments>
35
+ </tool_use>
36
+
37
+ The tool name should be the exact name of the tool you are using, and the arguments should be a JSON object containing the parameters required by that tool. For example:
38
+
39
+ <tool_use>
40
+ <name>say_hello</name>
41
+ <arguments>
42
+ {{
43
+ "content": "你好"
44
+ }}
45
+ </arguments>
46
+ </tool_use>
47
+
48
+ The result should be a string, which can represent a file or any other output type. You can use this result as input for the next action.
49
+
50
+ Always adhere to this format for the tool use to ensure proper parsing and execution.
51
+
52
+ ## Tool Use Available Tools
53
+ Above example were using notional tools that might not exist for you. You only have access to these tools:
54
+ {tools_description}
55
+
56
+ ## Tool Use Rules
57
+ Here are the rules you should always follow to solve your task:
58
+ 1. Always use the right arguments for the tools. Never use variable names as the action arguments, use the value instead.
59
+ 2. Call a tool only when needed: do not call the search agent if you do not need information, try to solve the task yourself.
60
+ 3. If no tool call is needed, just answer the question directly.
61
+ 4. Never re-do a tool call that you previously did with the exact same parameters.
62
+ 5. For tool use, MARK SURE use XML tag format as shown in the examples above. Do not use any other format.
63
+ 6. Parameter passing should never escape unicode, and this is done by default, do not convert Chinese to Unicode escape characters
64
+
65
+ # User Instructions
66
+ {user_system_prompt}
67
+
68
+ Now Begin! If you solve the task correctly, you will receive a reward of $1,000,000.
69
+ """
70
+
71
+ tools_use_example = """
72
+ Here are a few examples using notional tools:
73
+
74
+ ---
75
+ User: "What is the result of the following operation: 5 + 3 + 1294.678?"
76
+
77
+ Assistant: I can use the python_interpreter tool to calculate the result of the operation.
78
+ <tool_use>
79
+ <name>python_interpreter</name>
80
+ <arguments>{"code": "5 + 3 + 1294.678"}</arguments>
81
+ </tool_use>
82
+
83
+ User: {
84
+ "tool_name": "python_interpreter",
85
+ "result": ["1302.678"]
86
+ }
87
+
88
+ Assistant: The result of the operation is 1302.678.
89
+
90
+ ---
91
+ User: "Which city has the highest population , Guangzhou or Shanghai?"
92
+
93
+ Assistant: I can use the search tool to find the population of Guangzhou.
94
+ <tool_use>
95
+ <name>search</name>
96
+ <arguments>{"query": "Population Guangzhou"}</arguments>
97
+ </tool_use>
98
+
99
+ User: {
100
+ "tool_name": "search",
101
+ "result": ["Guangzhou has a population of 15 million inhabitants as of 2021."]
102
+ }
103
+
104
+ Assistant: I can use the search tool to find the population of Shanghai.
105
+ <tool_use>
106
+ <name>search</name>
107
+ <arguments>{"query": "Population Shanghai"}</arguments>
108
+ </tool_use>
109
+
110
+ User: {
111
+ "tool_name": "search",
112
+ "result": ["26 million (2019)"]
113
+ }
114
+ Assistant: The population of Shanghai is 26 million, while Guangzhou has a population of 15 million. Therefore, Shanghai has the highest population.
115
+ """
116
+
117
+ async def get_tools_prompt(mcp_clients, user_system_prompt) -> str:
118
+ all_tools = []
119
+ for client in mcp_clients:
120
+ tools = await client.list_server_tools()
121
+ all_tools.extend(tools)
122
+ return sys_prompt_4_mcp.format(tools_description="\n".join([tool.format_for_llm() for tool in all_tools]), user_system_prompt=user_system_prompt)
123
+
124
+ class Tool:
125
+
126
+ def __init__(self, name: str, description: str, input_schema: dict[str, Any]) -> None:
127
+ self.name: str = name
128
+ self.description: str = description
129
+ self.input_schema: dict[str, Any] = input_schema
130
+
131
+ def format_for_llm(self) -> str:
132
+ args_desc = []
133
+ if "properties" in self.input_schema:
134
+ for param_name, param_info in self.input_schema["properties"].items():
135
+ arg_desc = f"- {param_name}({param_info.get('type', 'Any')}): {param_info.get('description', '')}"
136
+ if param_name in self.input_schema.get("required", []):
137
+ arg_desc += " (required)"
138
+ args_desc.append(arg_desc)
139
+ return f"""
140
+ Tool: {self.name}
141
+ Description: {self.description}
142
+ Args_Info:
143
+ {chr(10).join(args_desc)}
144
+ """
145
+
146
+ class MCPClient:
147
+
148
+ def __init__(self, name: str, config: dict[str, Any]) -> None:
149
+ self.name: str = name
150
+ self.config: dict[str, Any] = config
151
+ self.stdio_context: Any | None = None
152
+ self.session: ClientSession | None = None
153
+ self.exit_stack: AsyncExitStack = AsyncExitStack()
154
+ self.tools = []
155
+
156
+ async def initialize(self) -> None:
157
+ if self.config.get('server_type') is None or self.config.get('server_type') == 'stdio':
158
+ command = (shutil.which("npx") if self.config["command"] == "npx" else self.config["command"])
159
+ if command is None: raise ValueError("The command must be a valid string and cannot be None.")
160
+ server_params = StdioServerParameters(
161
+ command=command,
162
+ args=self.config["args"],
163
+ env=self.config["env"] if self.config.get("env") else None,
164
+ )
165
+ stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
166
+ read, write = stdio_transport
167
+ self.session = await self.exit_stack.enter_async_context(ClientSession(read, write))
168
+ await self.session.initialize()
169
+ elif self.config['server_type'] == 'SSE':
170
+ sse_transport = await self.exit_stack.enter_async_context(sse_client(
171
+ url=self.config["url"],
172
+ headers=self.config["headers"],
173
+ timeout=self.config["timeout"],
174
+ sse_read_timeout=self.config["sse_read_timeout"]))
175
+ read, write = sse_transport
176
+ self.session = await self.exit_stack.enter_async_context(ClientSession(read, write))
177
+ await self.session.initialize()
178
+ elif self.config['server_type'] == 'Streamable HTTP':
179
+ stream_transport = await self.exit_stack.enter_async_context(streamablehttp_client(
180
+ url=self.config["url"],
181
+ headers=self.config["headers"],
182
+ timeout=self.config["timeout"],
183
+ sse_read_timeout=self.config["sse_read_timeout"]))
184
+ read, write, session_id = stream_transport
185
+ self.session = await self.exit_stack.enter_async_context(ClientSession(read, write))
186
+ await self.session.initialize()
187
+
188
+ async def list_server_tools(self) -> list[Any]:
189
+ if not self.session: raise RuntimeError(f"Server {self.name} not initialized")
190
+ if self.tools: return self.tools
191
+ tools_response = await self.session.list_tools()
192
+ for item in tools_response:
193
+ if isinstance(item, tuple) and item[0] == "tools":
194
+ self.tools.extend(Tool(tool.name, tool.description, tool.inputSchema) for tool in item[1])
195
+ return self.tools
196
+
197
+ async def execute_tool(
198
+ self,
199
+ tool_name: str,
200
+ arguments: dict[str, Any],
201
+ retries: int = 3,
202
+ delay: float = 1.0,
203
+ ) -> Any:
204
+ if not self.session: raise RuntimeError(f"Server {self.name} not initialized")
205
+ attempt = 0
206
+ args = arguments
207
+ while attempt < retries:
208
+ try:
209
+ log.info(f"Executing {tool_name}...")
210
+ result = await self.session.call_tool(tool_name, args)
211
+ return result
212
+ except Exception as e:
213
+ attempt += 1
214
+ log.warning(f"Error executing tool: {e}. Attempt {attempt} of {retries}.")
215
+ if attempt < retries:
216
+ log.info(f"Retrying in {delay} seconds...")
217
+ await asyncio.sleep(delay)
218
+ else:
219
+ log.error("Max retries reached. Failing.")
220
+ raise
221
+
222
+ async def cleanup(self) -> None:
223
+ await self.exit_stack.aclose()
224
+ self.session = None
225
+ self.stdio_context = None
226
+ self.exit_stack = None
227
+
228
+
229
+ async def mcp_tool_call(mcp_clients: MCPClient, xml_info: str) -> str:
230
+ if not mcp_clients: return xml_info
231
+ final_result = {
232
+ "tool_name": "",
233
+ "result": []
234
+ }
235
+ try:
236
+ tool_call = parse_tool_use(xml_info)
237
+ if "tool" in tool_call and "arguments" in tool_call:
238
+ log.info(f"Executing tool: {tool_call['tool']} With arguments: {tool_call['arguments']}")
239
+ for client in mcp_clients:
240
+ tools = await client.list_server_tools()
241
+ if any(tool.name == tool_call["tool"] for tool in tools):
242
+ final_result["tool_name"] = tool_call["tool"]
243
+ try:
244
+ result: CallToolResult = await client.execute_tool(tool_call["tool"], tool_call["arguments"])
245
+ text_result = []
246
+ image_result = []
247
+ tools_call_content = result.content
248
+ for content in tools_call_content:
249
+ if type(content) == TextContent:
250
+ try:
251
+ text_result.append(json.loads(content.text))
252
+ except Exception as e:
253
+ text_result.append(content.text)
254
+ elif type(content) == ImageContent:
255
+ image_result.append({"mime_type": content.mimeType, "data": content.data})
256
+ text_result.extend(image_result)
257
+ final_result["result"] = text_result
258
+ return final_result
259
+ except Exception as e:
260
+ log.exception(e)
261
+ error_msg = f"Error executing tool: {str(e)}"
262
+ final_result["result"] = [error_msg]
263
+ return final_result
264
+ return f"No server found with tool: {tool_call['tool']}"
265
+ return xml_info
266
+ except Exception as e:
267
+ log.exception(e)
268
+ error_msg = f"Error executing tool: {str(e)}"
269
+ final_result["result"] = [error_msg]
270
+ return final_result
271
+
272
+ def res_has_img(llm_response) -> bool:
273
+ if type(llm_response) == str: return False
274
+ response: [] = llm_response.get("result")
275
+ for rep in response:
276
+ if is_image_content(rep):
277
+ return True
278
+ return False
279
+
280
+ def is_image_content(content: dict) -> bool:
281
+ try:
282
+ if content.get("mime_type") and content.get("data"):
283
+ return True
284
+ return False
285
+ except Exception:
286
+ return False
287
+
288
+ @asynccontextmanager
289
+ async def init_mcp_clients(mcp_server_config: dict[str, Any]) -> list[MCPClient]:
290
+ mcp_clients = []
291
+ for name, sc in mcp_server_config["mcpServers"].items():
292
+ try:
293
+ mc = MCPClient(name, sc)
294
+ await mc.initialize()
295
+ mcp_clients.append(mc)
296
+ except Exception as e:
297
+ log.exception(f"Error initializing MCP servers: {e}")
298
+ yield mcp_clients
299
+ for client in mcp_clients:
300
+ try:
301
+ print(client.name)
302
+ await client.cleanup()
303
+ except Exception as e:
304
+ log.exception(f"Error unloading MCP servers: {e}")
305
+
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: UTF-8 -*-
3
+ __author__ = 'haoyang'
4
+ __date__ = '2025/5/22 15:57'
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: UTF-8 -*-
3
+ __author__ = 'haoyang'
4
+ __date__ = '2025/5/19 16:45'
5
+
6
+ import re
7
+
8
+ def extract_json_from_text(text):
9
+ """
10
+ 从混杂文本中提取第一个完整的 JSON 对象
11
+ """
12
+ import json
13
+
14
+ # 方法1:尝试直接解析
15
+ try:
16
+ return json.loads(text)
17
+ except json.JSONDecodeError:
18
+ pass
19
+
20
+ # 方法2:字符级括号匹配提取 JSON
21
+ start = None
22
+ brace_count = 0
23
+ for i, char in enumerate(text):
24
+ if char == '{':
25
+ if start is None:
26
+ start = i
27
+ brace_count += 1
28
+ elif char == '}':
29
+ brace_count -= 1
30
+ if brace_count == 0 and start is not None:
31
+ json_candidate = text[start:i + 1]
32
+ try:
33
+ return json.loads(json_candidate)
34
+ except json.JSONDecodeError:
35
+ start = None # 重置继续寻找下一个可能的 JSON
36
+
37
+ # 方法3:尝试 JSONP 格式
38
+ match = re.search(r'\((\{[\s\S]*\})\)', text)
39
+ if match:
40
+ try:
41
+ return json.loads(match.group(1))
42
+ except json.JSONDecodeError:
43
+ pass
44
+
45
+ return None
46
+
@@ -0,0 +1,32 @@
1
+ import json
2
+ import xml.etree.ElementTree as ET
3
+ import html
4
+
5
+
6
+ def parse_tool_use(xml_string):
7
+ tool_name = ''
8
+ try:
9
+ root = ET.fromstring(xml_string.strip())
10
+ if root.tag != "tool_use": raise ValueError("根标签必须是 <tool_use>")
11
+ tool_name = root.find("name").text.strip()
12
+ arguments_text = root.find("arguments").text.strip()
13
+ arguments_text = html.unescape(arguments_text)
14
+ arguments = json.loads(arguments_text)
15
+ return {
16
+ "tool": tool_name,
17
+ "arguments": arguments
18
+ }
19
+ except Exception as e:
20
+ raise ValueError(f"tool_use_{tool_name} 解析失败: {e}")
21
+
22
+ # 测试
23
+ if __name__ == '__main__':
24
+ xml_input = """
25
+ <tool_use>
26
+ <name>set</name>
27
+ <arguments>{"key": "weather_harbin", "value": "{\\"city\\":\\"哈尔滨市\\",\\"forecasts\\":[{\\"date\\":\\"2025-05-27\\",\\"week\\":\\"2\\",\\"dayweather\\":\\"晴\\",\\"nightweather\\":\\"晴\\",\\"daytemp\\":29,\\"nighttemp\\":15,\\"daywind\\":\\"南\\",\\"nightwind\\":\\"南\\",\\"daypower\\":1,\\"nightpower\\":3},{\\"date\\":\\"2025-05-28\\",\\"week\\":"3", \\"dayweather\\":"晴", \\"nightweather\\":"晴", \\"daytemp\\":"30", \\"nighttemp\\":"17"}]}"}</arguments>
28
+ </tool_use>
29
+ """
30
+ result = parse_tool_use(xml_input)
31
+ print("\n【结果】")
32
+ print(json.dumps(result, ensure_ascii=False, indent=2))
@@ -0,0 +1,13 @@
1
+ import re
2
+
3
+ def extract_all_xml_blocks(text):
4
+ return re.findall(r"<tool_use>.*?</tool_use>", text, re.DOTALL)
5
+ text = """
6
+ 一些内容...
7
+ 123
8
+ """
9
+
10
+ results = extract_all_xml_blocks(text)
11
+ print(results)
12
+ for xml_block in results:
13
+ print(xml_block)
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: UTF-8 -*-
3
+ """A lightweight async HTTP server based on aiohttp."""
4
+
5
+ __author__ = 'haoyang'
6
+ __date__ = '2025/5/30 09:54'
7
+
8
+ import asyncio
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Optional, Dict, Any
12
+
13
+ from aiohttp import web
14
+
15
+ from ctools import sys_info
16
+
17
+ DEFAULT_PORT = 8888
18
+
19
+ class AioHttpServer:
20
+ def __init__(self, port: int = DEFAULT_PORT, app: Optional[web.Application] = None, routes: Optional[web.RouteTableDef] = None, async_func = None):
21
+ """
22
+ Initialize the HTTP server.
23
+
24
+ Args:
25
+ port: Port number to listen on
26
+ app: Optional existing aiohttp Application instance
27
+ """
28
+ self.app = app or web.Application()
29
+ self.port = port
30
+ self.index_root = Path('./')
31
+ self.index_filename = 'index.html'
32
+ self.is_tpl = False
33
+ self.template_args: Dict[str, Any] = {}
34
+ self.redirect_url: Optional[str] = None
35
+ self.static_root = Path('./static')
36
+ self.download_root = Path('./download')
37
+ self.routes = routes
38
+ self.async_func = async_func
39
+
40
+ # Register routes
41
+ self.app.add_routes([
42
+ web.get('/', self.handle_index),
43
+ web.get('/index', self.handle_index),
44
+ web.get('/static/{filepath:.*}', self.handle_static),
45
+ web.get('/download/{filepath:.*}', self.handle_download)
46
+ ])
47
+ if self.routes:
48
+ self.app.add_routes(self.routes)
49
+
50
+ async def handle_index(self, request: web.Request) -> web.StreamResponse:
51
+ """Handle requests to the index page."""
52
+ if self.redirect_url:
53
+ return web.HTTPFound(self.redirect_url)
54
+
55
+ index_path = self.index_root / self.index_filename
56
+
57
+ if not index_path.exists():
58
+ return web.HTTPNotFound()
59
+
60
+ if self.is_tpl:
61
+ # If using templates, you might want to use a template engine here
62
+ return web.FileResponse(
63
+ index_path,
64
+ headers={'Content-Type': 'text/html'}
65
+ )
66
+ return web.FileResponse(index_path)
67
+
68
+ async def handle_static(self, request: web.Request) -> web.StreamResponse:
69
+ """Handle static file requests."""
70
+ filepath = Path(request.match_info['filepath'])
71
+ full_path = self.static_root / filepath
72
+
73
+ if not full_path.exists():
74
+ return web.HTTPNotFound()
75
+
76
+ return web.FileResponse(full_path)
77
+
78
+ async def handle_download(self, request: web.Request) -> web.StreamResponse:
79
+ """Handle file download requests."""
80
+ filepath = Path(request.match_info['filepath'])
81
+ full_path = self.download_root / filepath
82
+
83
+ if not full_path.exists():
84
+ return web.HTTPNotFound()
85
+
86
+ return web.FileResponse(
87
+ full_path,
88
+ headers={'Content-Disposition': f'attachment; filename="{filepath.name}"'}
89
+ )
90
+
91
+ def set_index(
92
+ self,
93
+ filename: str = 'index.html',
94
+ root: str = './',
95
+ is_tpl: bool = False,
96
+ redirect_url: Optional[str] = None,
97
+ **kwargs: Any
98
+ ) -> None:
99
+ """
100
+ Configure index page settings.
101
+
102
+ Args:
103
+ filename: Name of the index file
104
+ root: Root directory for index file
105
+ is_tpl: Whether the file is a template
106
+ redirect_url: URL to redirect to instead of serving index
107
+ kwargs: Additional template arguments
108
+ """
109
+ self.index_root = Path(root)
110
+ self.index_filename = filename
111
+ self.is_tpl = is_tpl
112
+ self.redirect_url = redirect_url
113
+ self.template_args = kwargs
114
+
115
+ def set_static(self, root: str = './static') -> None:
116
+ """Set the root directory for static files."""
117
+ self.static_root = Path(root)
118
+
119
+ def set_download(self, root: str = './download') -> None:
120
+ """Set the root directory for downloadable files."""
121
+ self.download_root = Path(root)
122
+
123
+ async def run(self) -> None:
124
+ """Run the server."""
125
+ if self.async_func:
126
+ await self.async_func()
127
+ print(
128
+ 'Server running at:\n'
129
+ f'\tLocal: http://localhost:{self.port}\n'
130
+ f'\tNetwork: http://{sys_info.get_local_ipv4()}:{self.port}',
131
+ file=sys.stderr
132
+ )
133
+ await web._run_app(
134
+ self.app,
135
+ port=self.port,
136
+ host='0.0.0.0'
137
+ )
138
+
139
+
140
+ def init_routes() -> web.RouteTableDef:
141
+ return web.RouteTableDef()
142
+
143
+ def init_server(routes: Optional[web.RouteTableDef] = None, app: Optional[web.Application] = None, port: int = DEFAULT_PORT, async_func = None) -> AioHttpServer:
144
+ """Initialize and return a new AioHttpServer instance."""
145
+ return AioHttpServer(port=port, app=app, routes=routes, async_func=async_func)
ctools/bottle_web_base.py CHANGED
@@ -6,8 +6,8 @@ import bottle
6
6
  from bottle import response, Bottle, request
7
7
 
8
8
  from ctools import ctoken
9
- from ctools.dict_wrapper import DictWrapper
10
9
  from ctools.api_result import R
10
+ from ctools.dict_wrapper import DictWrapper
11
11
  from ctools.sys_log import flog as log
12
12
 
13
13
  bottle.BaseRequest.MEMFILE_MAX = 1024 * 1024 * 50
@@ -22,7 +22,7 @@ def get_ws_modules():
22
22
  """
23
23
 
24
24
  """
25
- from ctools import bottle_web_base, token, bottle_webserver
25
+ from ctools import bottle_web_base, ctoken, bottle_webserver
26
26
  from ctools.api_result import R
27
27
 
28
28
  secret_key = "xxx"
@@ -35,7 +35,7 @@ def token_check():
35
35
 
36
36
  @app.post('/login')
37
37
  def login(params):
38
- return R.ok(token.gen_token({'username': 'xxx'}, secret_key, 3600))
38
+ return R.ok(ctoken.gen_token({'username': 'xxx'}, secret_key, 3600))
39
39
 
40
40
  @app.get('/demo')
41
41
  @bottle_web_base.rule('DOC:DOWNLOAD')
@@ -115,6 +115,7 @@ class CBottle:
115
115
  self.download_root = root
116
116
 
117
117
  def mount(self, context_path, app, **kwargs):
118
+ if not context_path: return
118
119
  self.bottle.mount(context_path, app, **kwargs)
119
120
 
120
121
  def init_bottle(app:Bottle=None, port=_default_port, quiet=False) -> CBottle:
ctools/cdebug.py CHANGED
@@ -14,6 +14,11 @@ class ProgramInterceptor:
14
14
  self.log_queue = Queue()
15
15
 
16
16
  def start(self):
17
+ if self.command[0] == "--log":
18
+ # 启动日志写入线程
19
+ log_thread = threading.Thread(target=self._write_log_thread, daemon=True)
20
+ log_thread.start()
21
+ self.command = self.command[1:]
17
22
  # 启动子进程
18
23
  self.process = subprocess.Popen(
19
24
  self.command,
@@ -24,10 +29,6 @@ class ProgramInterceptor:
24
29
  universal_newlines=True
25
30
  )
26
31
 
27
- # 启动日志写入线程
28
- log_thread = threading.Thread(target=self._write_log_thread, daemon=True)
29
- log_thread.start()
30
-
31
32
  # 记录初始信息
32
33
  self._enqueue_log("header", f"Command: {' '.join(self.command)}")
33
34
  self._enqueue_log("header", f"Start time: {datetime.now()}")
@@ -88,7 +89,9 @@ class ProgramInterceptor:
88
89
 
89
90
  # 等待日志写入完成
90
91
  self.log_queue.put(None) # 结束信号
91
- log_thread.join(timeout=2)
92
+ if hasattr(self, "log_thread") and isinstance(self.log_thread, threading.Thread):
93
+ if self.log_thread.is_alive():
94
+ self.log_thread.join(timeout=2)
92
95
 
93
96
  def _forward_stream(self, source, target, stream_name):
94
97
  """转发数据流并记录"""
ctools/ckafka.py CHANGED
@@ -4,12 +4,11 @@ __author__ = 'haoyang'
4
4
  __date__ = '2024/9/5 10:39'
5
5
 
6
6
  import time
7
- from threading import Thread, Lock
7
+ from threading import Thread
8
8
 
9
9
  from kafka import KafkaProducer, errors, KafkaConsumer
10
10
  from kafka.producer.future import FutureRecordMetadata
11
11
 
12
- from ctools import thread_pool
13
12
  from ctools.cjson import dumps
14
13
 
15
14
  """
ctools/credis.py CHANGED
@@ -8,6 +8,7 @@ from redis import Redis
8
8
 
9
9
  from ctools import date_utils, thread_pool, string_tools
10
10
 
11
+
11
12
  def init_pool(host: str = 'localhost', port: int = 6379, db: int = 0, password: str = None,
12
13
  username: str = None, decode_responses: bool = True, max_connections: int = 75,
13
14
  health_check_interval: int = 30, retry_count: int = 3) -> Redis:
ctools/ctoken.py CHANGED
@@ -4,6 +4,7 @@ __author__ = 'haoyang'
4
4
  __date__ = '2025/1/21 16:01'
5
5
 
6
6
  import time
7
+
7
8
  import jwt
8
9
  from bottle import request
9
10
 
ctools/czip.py CHANGED
@@ -6,6 +6,7 @@ __date__ = '2025/1/24 08:48'
6
6
  import io
7
7
  import os
8
8
  import time
9
+
9
10
  import pyzipper
10
11
 
11
12
  """
ctools/database.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import contextlib
2
2
  import datetime
3
- import math
4
3
 
4
+ import math
5
5
  from sqlalchemy import create_engine, Integer, Column, event
6
6
  from sqlalchemy.ext.declarative import declarative_base
7
7
  from sqlalchemy.orm import sessionmaker, Session
ctools/douglas_rarefy.py CHANGED
@@ -4,7 +4,6 @@ __author__ = 'haoyang'
4
4
  __date__ = '2024/9/19 14:02'
5
5
 
6
6
  import math
7
-
8
7
  from jsonpath_ng import parser
9
8
 
10
9
  from ctools import cjson
ctools/http_utils.py CHANGED
@@ -16,7 +16,7 @@ def get(url, params=None, headers=None):
16
16
 
17
17
  def post(url, data=None, json=None, headers=None, files=None):
18
18
  result = ""
19
- response = requests.post(url, data=data, json=json, files=files, headers=headers, timeout=60, verify=False)
19
+ response = requests.post(url, data=data, json=json, files=files, headers=headers, timeout=600, verify=False)
20
20
  response.raise_for_status()
21
21
  if response.status_code == 200:
22
22
  result = response.content
ctools/imgDialog.py CHANGED
@@ -6,6 +6,7 @@ from tkinter import ttk
6
6
  import requests
7
7
  from PIL import Image, ImageTk
8
8
 
9
+
9
10
  def showImageTip(root, title, imagePath, tips):
10
11
  # 创建一个Tk对象
11
12
  if root:
ctools/metrics.py CHANGED
@@ -3,6 +3,7 @@ import threading
3
3
  from enum import Enum
4
4
 
5
5
  from prometheus_client import Counter, Gauge, Summary, Histogram, start_http_server
6
+
6
7
  from ctools import call, cjson, sys_log, work_path
7
8
 
8
9
  log = sys_log.flog
ctools/mqtt_utils.py CHANGED
@@ -2,11 +2,12 @@ import time
2
2
  from enum import Enum
3
3
  from typing import Dict
4
4
 
5
- from ctools.dict_wrapper import DictWrapper as DictToObj
6
5
  from paho.mqtt import client as mqtt
7
6
  from paho.mqtt.enums import CallbackAPIVersion
8
7
 
9
8
  from ctools import sys_log, cjson, string_tools, sys_info, date_utils, sm_tools, thread_pool
9
+ from ctools.dict_wrapper import DictWrapper as DictToObj
10
+
10
11
 
11
12
  class MQTTEvent(Enum):
12
13
 
ctools/pty_tools.py CHANGED
@@ -2,11 +2,12 @@ import _thread
2
2
  import queue
3
3
  import time
4
4
 
5
- # 伪终端交互
6
-
7
5
  from winpty import PtyProcess
8
6
 
9
7
 
8
+ # 伪终端交互
9
+
10
+
10
11
  class Code:
11
12
  SUCCESS = 200
12
13
  FAIL = 201
ctools/rsa.py CHANGED
@@ -1,9 +1,9 @@
1
1
  import base64
2
2
 
3
3
  from Crypto.Cipher import PKCS1_OAEP
4
+ from Crypto.Hash import SHA256
4
5
  from Crypto.PublicKey import RSA
5
6
  from Crypto.Signature import pkcs1_15
6
- from Crypto.Hash import SHA256
7
7
 
8
8
  from ctools import work_path, cjson
9
9
 
ctools/sys_log.py CHANGED
@@ -76,6 +76,9 @@ class GlobalLogger(object):
76
76
  def flush(self):
77
77
  pass
78
78
 
79
+ def fileno(self):
80
+ return sys.__stdout__.fileno()
81
+
79
82
 
80
83
  @call.init
81
84
  def _init_log() -> None:
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gomyck-tools
3
- Version: 1.3.3
3
+ Version: 1.3.5
4
4
  Summary: A tools collection for python development by hao474798383
5
5
  Author-email: gomyck <hao474798383@163.com>
6
6
  License-Expression: Apache-2.0
7
- Requires-Python: >=3.10
7
+ Requires-Python: >=3.11
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
10
  Requires-Dist: jsonpickle~=3.4.2
@@ -32,16 +32,10 @@ Requires-Dist: redis==5.2.1
32
32
  Provides-Extra: db
33
33
  Requires-Dist: sqlalchemy>=2.0; extra == "db"
34
34
  Requires-Dist: asyncpg>=0.28; extra == "db"
35
- Provides-Extra: dev
36
- Requires-Dist: pytest>=7.0; extra == "dev"
37
- Requires-Dist: black>=24.0; extra == "dev"
38
- Requires-Dist: mypy>=1.0; extra == "dev"
39
- Provides-Extra: full
40
- Requires-Dist: sqlalchemy>=2.0; extra == "full"
41
- Requires-Dist: asyncpg>=0.28; extra == "full"
42
- Requires-Dist: pytest>=7.0; extra == "full"
43
- Requires-Dist: black>=24.0; extra == "full"
44
- Requires-Dist: mypy>=1.0; extra == "full"
35
+ Provides-Extra: office
36
+ Requires-Dist: python-docx==1.1.2; extra == "office"
37
+ Provides-Extra: auto-ui
38
+ Requires-Dist: pynput==1.7.7; extra == "auto-ui"
45
39
  Dynamic: license-file
46
40
 
47
41
  # Gomyck-Tools
@@ -1,47 +1,48 @@
1
1
  ctools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  ctools/aes_tools.py,sha256=L5Jg4QtVTdIxHe9zEpR8oMQx0IrYK68vjEYb_RmkhPA,699
3
+ ctools/aio_web_server.py,sha256=NRishyg8cbaC9yugOnefnDjYdjlE2zwB0gzRJV5Tr6M,4353
3
4
  ctools/api_result.py,sha256=UeQXI_zuZB-uY5qECTpz1fC7EGy82yGQqWMx20tyRTw,1572
4
5
  ctools/application.py,sha256=DcuSt2m8cDuSftx6eKfJ5gA6_F9dDlzkj0K86EG4F7s,15884
5
6
  ctools/b64.py,sha256=_BdhX3p3-MaSSlU2wivN5qPxQfacR3VRBr1WC456tU0,194
6
7
  ctools/bashPath.py,sha256=BCN_EhYzqvwsxYso81omMNd3SbEociwSOyb9kLvu8V4,337
7
- ctools/bottle_web_base.py,sha256=8Nb9CF8OqWl4HNsiiZuFOH509yAqViqYcC36vFcAI5A,6328
8
- ctools/bottle_webserver.py,sha256=l7t_sN4ayywD1sR0kzuhGioOuaqGR9VhJh7e6Gbd6aE,4642
8
+ ctools/bottle_web_base.py,sha256=JkX9azYXmVFaWOQKi8m-dT-gRjbgsxdKunRRu2lgLYU,6328
9
+ ctools/bottle_webserver.py,sha256=tJtMSZ7r6jiIk2NzrX4uSoRQnTqueM6AYdGKd9AhgOg,4676
9
10
  ctools/bottle_websocket.py,sha256=zqCE1rGlMeC9oxFOULNd137IWIhdetq83Oq5OoH_zGI,1953
10
11
  ctools/browser_element_tools.py,sha256=IFR_tWu5on0LxhuC_4yT6EOjwCsC-juIoU8KQRDqR7E,9952
11
12
  ctools/call.py,sha256=BCr8wzt5qd70okv8IZn-9-EpjywleZgvA3u1vfZ_Kt8,1581
12
- ctools/cdebug.py,sha256=4PWZxrUcfTOljK7xXSiQ9iXddYwsKKeq96RGUFNqNkg,3807
13
+ ctools/cdebug.py,sha256=_mihZRCEx_bi7Kv_QPjP4MPLNFrl-GR1Y_irTgOP7OU,4021
13
14
  ctools/cftp.py,sha256=SkHPDvKu58jnMnl68u5WxWEiFWsm2C0CGa5_GR_Obcw,2481
14
15
  ctools/cjson.py,sha256=d0RZ53b-M18OudRFGRtPCvvGofJcaLHdbNlTCemyJag,1365
15
- ctools/ckafka.py,sha256=EiiGCFkSbq8yRjQKVjc2_V114hKb8fJAVIOks_XbQ3w,5944
16
+ ctools/ckafka.py,sha256=8zMTS6iCCvxi5Ez4z9mcaBoVX7tb4R5tfyjG-xlM1HQ,5907
16
17
  ctools/compile_tools.py,sha256=Nybh3vnkurIKnPnubdYzigjnzFu4GaTMKPvqFdibxmE,510
17
18
  ctools/console.py,sha256=EZumuyynwteKUhUxB_XoulHswDxHd75OQB34RiZ-OBM,1807
18
19
  ctools/coord_trans.py,sha256=pzIHxC4aLwvOF3eJG47Dda3vIq-Zp42xnu_FwILDflU,3951
19
- ctools/credis.py,sha256=sW7yDQvxa7B4dWvGwUH7GROq-7ElRMDhFT6g2C8ryfE,4522
20
+ ctools/credis.py,sha256=beTwpz4EGfy871rC9rOl1FccIOTXBXzp7Fj7Rmhx2VM,4523
20
21
  ctools/cron_lite.py,sha256=CUqdtO02VnYUWcw6t6Jr7v2dKKhx3_G7CDAtlWnirBE,8232
21
- ctools/ctoken.py,sha256=NZSBGF3lJajJFLRIZoeXmpp8h5cKM0dAH2weySgeORc,882
22
+ ctools/ctoken.py,sha256=CdHm6-ykBLh7Lv8ZRMunSW40qMTkRH0ITeMLuG9z1ts,883
22
23
  ctools/cword.py,sha256=ZRzAFn96yjo-hAbZuGIm4DoBAL2y8tFySWZ5xbYgY6Q,857
23
- ctools/czip.py,sha256=8VQ420KgMF09U8McSXTkaAz0jd0Zzm6qazf3iJADQI4,4674
24
- ctools/database.py,sha256=4j8pPBCJ8DwZrWpeBEiLVtYDMjzkDgkPGQTkOD_kzKI,6415
24
+ ctools/czip.py,sha256=8LBoB4PsjN2HGga5sWk1FSigP21kIrRrzK63l92H0R8,4675
25
+ ctools/database.py,sha256=MGAcffLEHDRpesQLIzQWQ-WFmqSXq6dNjqJsiQo4Ric,6415
25
26
  ctools/date_utils.py,sha256=h3rvlw_K2F0QTac2Zat_1us76R0P-Qj6_6NeQPfM3VE,1697
26
27
  ctools/dict_wrapper.py,sha256=otxDX0CCKbBCVFtASweo5VEv6_ettH-CptA6azX1mJI,460
27
- ctools/douglas_rarefy.py,sha256=43WRjGGsQ_o1yPEXypA1Xv_yuo90RVo7qaYGRslx5gQ,4890
28
+ ctools/douglas_rarefy.py,sha256=iRdUdhmaMmdfXPzoleQaGMrtcAoHUDRPHYtWStTx4U4,4889
28
29
  ctools/download_tools.py,sha256=oJbG12Hojd0J17sAlvMU480P3abi4_AB9oZkEBGFPuo,1930
29
30
  ctools/enums.py,sha256=QbHa3j7j4-BDdwaga5Y0nYfA2uNSVJDHumYdIZdKVkM,118
30
31
  ctools/ex.py,sha256=_UtbmDLrC7uZsoBtTdecuCZAlf2DA7fvojUf5fGZDVo,795
31
32
  ctools/excelOpt.py,sha256=q3HLAb1JScTrMCvx_x-4WWnqKhyTEzQ-m5vtqFy8NZU,1138
32
33
  ctools/html_soup.py,sha256=rnr8M3gn3gQGo-wNaNFXDjdzp8AAkv9o4yqfIIfO-zw,1567
33
- ctools/http_utils.py,sha256=dG26aci1_YxAyKwYqMKFw4wZAryLkDyvnQ3hURjB6Lk,768
34
+ ctools/http_utils.py,sha256=cx0FRnPLFdJ0mF9UYphl40SZj68fqG30Q0udku9hZIE,769
34
35
  ctools/images_tools.py,sha256=TapXYCPqC7GesgrALecxxa_ApuT_dxUG5fqQIJF2bNY,670
35
- ctools/imgDialog.py,sha256=zFeyPmpnEn9Ih7-yuJJrePqW8Myj3jC9UYMTDk-umTs,1385
36
- ctools/metrics.py,sha256=Ld2OAeJLgXo66zIIn5eeD1AFIxTWw8JJeny--ge--6c,5247
37
- ctools/mqtt_utils.py,sha256=ZWSZiiNJLLlkHF95S6LmRmkYt-iIL4K73VdN3b1HaHw,10702
36
+ ctools/imgDialog.py,sha256=5r_XTPLyZj3Oa3HhwF2aQhziE74eFXe7aJZNndpXh18,1386
37
+ ctools/metrics.py,sha256=o_KSm_kWv-ipqzXSCUQr0kVILqqYrm5EkuBqKmNwt9w,5248
38
+ ctools/mqtt_utils.py,sha256=uxrDzIAtdlmpTZgBnoR8JXPCBE13UeGhMCBEuO9GD74,10703
38
39
  ctools/obj.py,sha256=GYS1B8NyjtUIh0HlK9r8avC2eGbK2SJac4C1CGnfGhI,479
39
40
  ctools/pacth.py,sha256=MJ9Du-J9Gv62y4cZKls1jKbl5a5kL2y9bD-gzYUCveQ,2604
40
41
  ctools/plan_area_tools.py,sha256=pySri43bVfkHjzlKujML-Nk8B3QLxuYv5KJMha-MLmU,3311
41
42
  ctools/process_pool.py,sha256=1TuZySUbQjgYYcuwis54DIwQTimWvTLNahSra7Ia8Ps,951
42
- ctools/pty_tools.py,sha256=KI3dOyv2JLZmU1VfD1aLMq9r9d5VCu3TdtcezZayBEI,1622
43
+ ctools/pty_tools.py,sha256=r3-MF5hkFQ7ZAGlrvUKtCEeQS1V2yWW4JDlPrFZXCkc,1623
43
44
  ctools/resource_bundle_tools.py,sha256=wA4fmD_ZEcrpcvUZKa60uDDX-nNQSVz1nBh0A2GVuTI,3796
44
- ctools/rsa.py,sha256=c0q7oStlpSfTxmn900XMDjyOGS1A7tVsUIocr0nL2UI,2259
45
+ ctools/rsa.py,sha256=6WKG3WCrQBPnUsZDfkd-pKwsYHypq8o2wC1uZFxEheo,2259
45
46
  ctools/screenshot_tools.py,sha256=KoljfgqW5x9aLwKdIfz0vR6v-fX4XjE92HudkDxC2hE,4539
46
47
  ctools/sign.py,sha256=JOkgpgsMbk7T3c3MOj1U6eiEndUG9XQ-uIX9e615A_Y,566
47
48
  ctools/sm_tools.py,sha256=R0m52TQE-CT7pvGTP27UWNCfdzpQ8C-ALz7p0mnOnLU,1672
@@ -49,7 +50,7 @@ ctools/snow_id.py,sha256=hYinnRN-aOule4_9vfgXB7XnsU-56cIS3PhzAwWBc5E,2270
49
50
  ctools/str_diff.py,sha256=QUtXOfsRLTFozH_zByqsC39JeuG3eZtrwGVeLyaHYUI,429
50
51
  ctools/string_tools.py,sha256=itK59W4Ed4rQzuyHuioNgDRUcBlfb4ZoZnwmS9cJxiI,1887
51
52
  ctools/sys_info.py,sha256=NvKCuBlWHHiW4bDI4tYZUo3QusvODm1HlW6aAkrllnE,4248
52
- ctools/sys_log.py,sha256=oqb1S41LosdeZxtogFVgDk8R4sjiHhUeYJLCzHd728E,2805
53
+ ctools/sys_log.py,sha256=GO7wBCkF_R35WwLJ7x5lW8Dz0KeZExlbsE_yGspRH1Y,2861
53
54
  ctools/thread_pool.py,sha256=Mt60XMhs-nk-hbkPo8NA7wQ4RxRLZTk4X6vh5Wn3WEw,944
54
55
  ctools/upload_tools.py,sha256=sqe6K3ZWiyY58pFE5IO5mNaS1znnS7U4c4UqY8noED4,1068
55
56
  ctools/win_canvas.py,sha256=PAxI4i1jalfree9d1YG4damjc2EzaHZrgHZCTgk2GiM,2530
@@ -57,8 +58,18 @@ ctools/win_control.py,sha256=35f9x_ijSyc4ZDkcT32e9ZIhr_ffNxadynrQfFuIdYo,3489
57
58
  ctools/word_fill.py,sha256=xeo-P4DOjQUqd-o9XL3g66wQrE2diUPGwFywm8TdVyw,18210
58
59
  ctools/word_fill_entity.py,sha256=eX3G0Gy16hfGpavQSEkCIoKDdTnNgRRJrFvKliETZK8,985
59
60
  ctools/work_path.py,sha256=OmfYu-Jjg2huRY6Su8zJ_2EGFFhtBZFbobYTwbjJtG4,1817
60
- gomyck_tools-1.3.3.dist-info/licenses/LICENSE,sha256=X25ypfH9E6VTht2hcO8k7LCSdHUcoG_ALQt80jdYZfY,547
61
- gomyck_tools-1.3.3.dist-info/METADATA,sha256=lGQt_tpcOP_1PQ1KTpb3_0ZBKxbXOkI8tuJfrnPk9qU,1744
62
- gomyck_tools-1.3.3.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
63
- gomyck_tools-1.3.3.dist-info/top_level.txt,sha256=-MiIH9FYRVKp1i5_SVRkaI-71WmF1sZSRrNWFU9ls3s,7
64
- gomyck_tools-1.3.3.dist-info/RECORD,,
61
+ ctools/ai/__init__.py,sha256=gTYAICILq48icnFbg0HCbsQO8PbU02EDOQ0JeMvfqTY,98
62
+ ctools/ai/env_config.py,sha256=hOuDVjyK00Ioq1G2z9Plf20TQha0tMV-VHWKAeCv16s,990
63
+ ctools/ai/llm_chat.py,sha256=E4ivclYISDo0DLt-lIRrtv-44qtQuWD9LE7wsivWDZw,8487
64
+ ctools/ai/llm_client.py,sha256=6hifeqUnLeYOLtSn1tfx5MtUrwJuFm7WelbKLtR0U0A,4758
65
+ ctools/ai/mcp/__init__.py,sha256=gTYAICILq48icnFbg0HCbsQO8PbU02EDOQ0JeMvfqTY,98
66
+ ctools/ai/mcp/mcp_client.py,sha256=HLoGN-yCL5vMxwxcZMmXQyWof-nmp1KjrPrXQ46KGQs,11525
67
+ ctools/ai/tools/__init__.py,sha256=gPc-ViRgtFlfX7JUbk5wQZ3wkJ5Ylh14CIqPwa83VPs,98
68
+ ctools/ai/tools/json_extract.py,sha256=R_cVAUJG78cO5-irREKaFq-QXI3w7T1soORVv9Y8-aw,1039
69
+ ctools/ai/tools/tool_use_xml_parse.py,sha256=ZS_9SPxI6ZxwgSOl0QQV5G7HCE72l1rrrkJgUid1NyI,1302
70
+ ctools/ai/tools/xml_extract.py,sha256=KTzsrJZDKmpJ-92yTO6z5nJ9a183ngF6ikTb4oiIjW4,246
71
+ gomyck_tools-1.3.5.dist-info/licenses/LICENSE,sha256=X25ypfH9E6VTht2hcO8k7LCSdHUcoG_ALQt80jdYZfY,547
72
+ gomyck_tools-1.3.5.dist-info/METADATA,sha256=Gt3-DuFNL_xZsDAGWt5L4iu0ADLnNz2QbIN-ha8Zdtk,1501
73
+ gomyck_tools-1.3.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
74
+ gomyck_tools-1.3.5.dist-info/top_level.txt,sha256=-MiIH9FYRVKp1i5_SVRkaI-71WmF1sZSRrNWFU9ls3s,7
75
+ gomyck_tools-1.3.5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.7.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5