gomyck-tools 1.3.4__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 +4 -0
- ctools/ai/env_config.py +33 -0
- ctools/ai/llm_chat.py +174 -0
- ctools/ai/llm_client.py +120 -0
- ctools/ai/mcp/__init__.py +4 -0
- ctools/ai/mcp/mcp_client.py +305 -0
- ctools/ai/tools/__init__.py +4 -0
- ctools/ai/tools/json_extract.py +46 -0
- ctools/ai/tools/tool_use_xml_parse.py +32 -0
- ctools/ai/tools/xml_extract.py +13 -0
- ctools/aio_web_server.py +145 -0
- ctools/bottle_web_base.py +1 -1
- ctools/bottle_webserver.py +3 -2
- ctools/cdebug.py +8 -5
- ctools/ckafka.py +1 -2
- ctools/credis.py +1 -0
- ctools/ctoken.py +1 -0
- ctools/czip.py +1 -0
- ctools/database.py +1 -1
- ctools/douglas_rarefy.py +0 -1
- ctools/http_utils.py +1 -1
- ctools/imgDialog.py +1 -0
- ctools/metrics.py +1 -0
- ctools/mqtt_utils.py +2 -1
- ctools/pty_tools.py +3 -2
- ctools/rsa.py +1 -1
- {gomyck_tools-1.3.4.dist-info → gomyck_tools-1.3.5.dist-info}/METADATA +6 -12
- {gomyck_tools-1.3.4.dist-info → gomyck_tools-1.3.5.dist-info}/RECORD +31 -20
- {gomyck_tools-1.3.4.dist-info → gomyck_tools-1.3.5.dist-info}/WHEEL +1 -1
- {gomyck_tools-1.3.4.dist-info → gomyck_tools-1.3.5.dist-info}/licenses/LICENSE +0 -0
- {gomyck_tools-1.3.4.dist-info → gomyck_tools-1.3.5.dist-info}/top_level.txt +0 -0
ctools/ai/__init__.py
ADDED
ctools/ai/env_config.py
ADDED
@@ -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
|
+
|
ctools/ai/llm_client.py
ADDED
@@ -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,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,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)
|
ctools/aio_web_server.py
ADDED
@@ -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
|
ctools/bottle_webserver.py
CHANGED
@@ -22,7 +22,7 @@ def get_ws_modules():
|
|
22
22
|
"""
|
23
23
|
|
24
24
|
"""
|
25
|
-
from ctools import bottle_web_base,
|
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(
|
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.
|
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
|
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
ctools/czip.py
CHANGED
ctools/database.py
CHANGED
ctools/douglas_rarefy.py
CHANGED
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=
|
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
ctools/metrics.py
CHANGED
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
ctools/rsa.py
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: gomyck-tools
|
3
|
-
Version: 1.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.
|
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:
|
36
|
-
Requires-Dist:
|
37
|
-
|
38
|
-
Requires-Dist:
|
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=
|
8
|
-
ctools/bottle_webserver.py,sha256=
|
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=
|
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=
|
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=
|
20
|
+
ctools/credis.py,sha256=beTwpz4EGfy871rC9rOl1FccIOTXBXzp7Fj7Rmhx2VM,4523
|
20
21
|
ctools/cron_lite.py,sha256=CUqdtO02VnYUWcw6t6Jr7v2dKKhx3_G7CDAtlWnirBE,8232
|
21
|
-
ctools/ctoken.py,sha256=
|
22
|
+
ctools/ctoken.py,sha256=CdHm6-ykBLh7Lv8ZRMunSW40qMTkRH0ITeMLuG9z1ts,883
|
22
23
|
ctools/cword.py,sha256=ZRzAFn96yjo-hAbZuGIm4DoBAL2y8tFySWZ5xbYgY6Q,857
|
23
|
-
ctools/czip.py,sha256=
|
24
|
-
ctools/database.py,sha256=
|
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=
|
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=
|
34
|
+
ctools/http_utils.py,sha256=cx0FRnPLFdJ0mF9UYphl40SZj68fqG30Q0udku9hZIE,769
|
34
35
|
ctools/images_tools.py,sha256=TapXYCPqC7GesgrALecxxa_ApuT_dxUG5fqQIJF2bNY,670
|
35
|
-
ctools/imgDialog.py,sha256=
|
36
|
-
ctools/metrics.py,sha256=
|
37
|
-
ctools/mqtt_utils.py,sha256=
|
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=
|
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=
|
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
|
@@ -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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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,,
|
File without changes
|
File without changes
|