sunholo 0.141.1__py3-none-any.whl → 0.143.1__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.
- sunholo/agents/flask/vac_routes.py +300 -1
- sunholo/invoke/async_class.py +98 -8
- sunholo/mcp/__init__.py +20 -0
- sunholo/mcp/mcp_manager.py +98 -0
- sunholo/mcp/vac_mcp_server.py +259 -0
- {sunholo-0.141.1.dist-info → sunholo-0.143.1.dist-info}/METADATA +6 -5
- {sunholo-0.141.1.dist-info → sunholo-0.143.1.dist-info}/RECORD +11 -9
- {sunholo-0.141.1.dist-info → sunholo-0.143.1.dist-info}/WHEEL +0 -0
- {sunholo-0.141.1.dist-info → sunholo-0.143.1.dist-info}/entry_points.txt +0 -0
- {sunholo-0.141.1.dist-info → sunholo-0.143.1.dist-info}/licenses/LICENSE.txt +0 -0
- {sunholo-0.141.1.dist-info → sunholo-0.143.1.dist-info}/top_level.txt +0 -0
@@ -6,6 +6,8 @@ import random
|
|
6
6
|
from functools import partial
|
7
7
|
import inspect
|
8
8
|
import asyncio
|
9
|
+
from typing import Dict, List, Optional, Callable, Any
|
10
|
+
|
9
11
|
|
10
12
|
from ..chat_history import extract_chat_history_with_cache, extract_chat_history_async_cached
|
11
13
|
from ...qna.parsers import parse_output
|
@@ -29,6 +31,21 @@ try:
|
|
29
31
|
except ImportError:
|
30
32
|
PubSubManager = None
|
31
33
|
|
34
|
+
try:
|
35
|
+
from ...mcp.mcp_manager import MCPClientManager
|
36
|
+
except ImportError:
|
37
|
+
MCPClientManager = None
|
38
|
+
|
39
|
+
try:
|
40
|
+
from ...mcp.vac_mcp_server import VACMCPServer
|
41
|
+
from mcp.server.models import InitializationOptions
|
42
|
+
from mcp import JSONRPCMessage, ErrorData, INTERNAL_ERROR
|
43
|
+
except ImportError:
|
44
|
+
VACMCPServer = None
|
45
|
+
InitializationOptions = None
|
46
|
+
JSONRPCMessage = None
|
47
|
+
|
48
|
+
|
32
49
|
# Cache dictionary to store validated API keys
|
33
50
|
api_key_cache = {}
|
34
51
|
cache_duration = timedelta(minutes=5) # Cache duration
|
@@ -61,11 +78,30 @@ if __name__ == "__main__":
|
|
61
78
|
stream_interpreter: callable,
|
62
79
|
vac_interpreter:callable=None,
|
63
80
|
additional_routes:dict=None,
|
81
|
+
mcp_servers: List[Dict[str, Any]] = None,
|
64
82
|
async_stream:bool=False,
|
65
|
-
add_langfuse_eval:bool=True
|
83
|
+
add_langfuse_eval:bool=True,
|
84
|
+
enable_mcp_server:bool=False):
|
66
85
|
self.app = app
|
67
86
|
self.stream_interpreter = stream_interpreter
|
68
87
|
self.vac_interpreter = vac_interpreter or partial(self.vac_interpreter_default)
|
88
|
+
|
89
|
+
# MCP client initialization
|
90
|
+
self.mcp_servers = mcp_servers or []
|
91
|
+
self.mcp_client_manager = MCPClientManager()
|
92
|
+
# Initialize MCP connections
|
93
|
+
if self.mcp_servers and self.mcp_client_manager:
|
94
|
+
asyncio.create_task(self._initialize_mcp_servers())
|
95
|
+
|
96
|
+
# MCP server initialization
|
97
|
+
self.enable_mcp_server = enable_mcp_server
|
98
|
+
self.vac_mcp_server = None
|
99
|
+
if self.enable_mcp_server and VACMCPServer:
|
100
|
+
self.vac_mcp_server = VACMCPServer(
|
101
|
+
stream_interpreter=self.stream_interpreter,
|
102
|
+
vac_interpreter=self.vac_interpreter
|
103
|
+
)
|
104
|
+
|
69
105
|
self.additional_routes = additional_routes if additional_routes is not None else []
|
70
106
|
self.async_stream = async_stream
|
71
107
|
self.add_langfuse_eval = add_langfuse_eval
|
@@ -129,6 +165,18 @@ if __name__ == "__main__":
|
|
129
165
|
# OpenAI compatible endpoint
|
130
166
|
self.app.route('/openai/v1/chat/completions', methods=['POST'])(self.handle_openai_compatible_endpoint)
|
131
167
|
self.app.route('/openai/v1/chat/completions/<vector_name>', methods=['POST'])(self.handle_openai_compatible_endpoint)
|
168
|
+
|
169
|
+
# MCP client routes
|
170
|
+
if self.mcp_servers:
|
171
|
+
self.app.route('/mcp/tools', methods=['GET'])(self.handle_mcp_list_tools)
|
172
|
+
self.app.route('/mcp/tools/<server_name>', methods=['GET'])(self.handle_mcp_list_tools)
|
173
|
+
self.app.route('/mcp/call', methods=['POST'])(self.handle_mcp_call_tool)
|
174
|
+
self.app.route('/mcp/resources', methods=['GET'])(self.handle_mcp_list_resources)
|
175
|
+
self.app.route('/mcp/resources/read', methods=['POST'])(self.handle_mcp_read_resource)
|
176
|
+
|
177
|
+
# MCP server endpoint
|
178
|
+
if self.enable_mcp_server and self.vac_mcp_server:
|
179
|
+
self.app.route('/mcp', methods=['POST', 'GET'])(self.handle_mcp_server)
|
132
180
|
|
133
181
|
self.register_additional_routes()
|
134
182
|
|
@@ -803,3 +851,254 @@ if __name__ == "__main__":
|
|
803
851
|
except Exception as e:
|
804
852
|
raise Exception(f'File upload failed: {str(e)}')
|
805
853
|
|
854
|
+
async def _initialize_mcp_servers(self):
|
855
|
+
"""Initialize connections to configured MCP servers."""
|
856
|
+
for server_config in self.mcp_servers:
|
857
|
+
try:
|
858
|
+
await self.mcp_client_manager.connect_to_server(
|
859
|
+
server_name=server_config["name"],
|
860
|
+
command=server_config["command"],
|
861
|
+
args=server_config.get("args", [])
|
862
|
+
)
|
863
|
+
log.info(f"Connected to MCP server: {server_config['name']}")
|
864
|
+
except Exception as e:
|
865
|
+
log.error(f"Failed to connect to MCP server {server_config['name']}: {e}")
|
866
|
+
|
867
|
+
|
868
|
+
def handle_mcp_list_tools(self, server_name: Optional[str] = None):
|
869
|
+
"""List available MCP tools."""
|
870
|
+
async def get_tools():
|
871
|
+
tools = await self.mcp_client_manager.list_tools(server_name)
|
872
|
+
return [
|
873
|
+
{
|
874
|
+
"name": tool.name,
|
875
|
+
"description": tool.description,
|
876
|
+
"inputSchema": tool.inputSchema,
|
877
|
+
"server": tool.metadata.get("server") if tool.metadata else server_name
|
878
|
+
}
|
879
|
+
for tool in tools
|
880
|
+
]
|
881
|
+
|
882
|
+
# Run async in sync context
|
883
|
+
loop = asyncio.new_event_loop()
|
884
|
+
asyncio.set_event_loop(loop)
|
885
|
+
try:
|
886
|
+
tools = loop.run_until_complete(get_tools())
|
887
|
+
return jsonify({"tools": tools})
|
888
|
+
finally:
|
889
|
+
loop.close()
|
890
|
+
|
891
|
+
def handle_mcp_call_tool(self):
|
892
|
+
"""Call an MCP tool."""
|
893
|
+
data = request.get_json()
|
894
|
+
server_name = data.get("server")
|
895
|
+
tool_name = data.get("tool")
|
896
|
+
arguments = data.get("arguments", {})
|
897
|
+
|
898
|
+
if not server_name or not tool_name:
|
899
|
+
return jsonify({"error": "Missing 'server' or 'tool' parameter"}), 400
|
900
|
+
|
901
|
+
async def call_tool():
|
902
|
+
try:
|
903
|
+
result = await self.mcp_client_manager.call_tool(server_name, tool_name, arguments)
|
904
|
+
|
905
|
+
# Convert result to JSON-serializable format
|
906
|
+
if hasattr(result, 'content'):
|
907
|
+
# Handle different content types
|
908
|
+
if hasattr(result.content, 'text'):
|
909
|
+
return {"result": result.content.text}
|
910
|
+
elif hasattr(result.content, 'data'):
|
911
|
+
return {"result": result.content.data}
|
912
|
+
else:
|
913
|
+
return {"result": str(result.content)}
|
914
|
+
else:
|
915
|
+
return {"result": str(result)}
|
916
|
+
|
917
|
+
except Exception as e:
|
918
|
+
return {"error": str(e)}
|
919
|
+
|
920
|
+
loop = asyncio.new_event_loop()
|
921
|
+
asyncio.set_event_loop(loop)
|
922
|
+
try:
|
923
|
+
result = loop.run_until_complete(call_tool())
|
924
|
+
if "error" in result:
|
925
|
+
return jsonify(result), 500
|
926
|
+
return jsonify(result)
|
927
|
+
finally:
|
928
|
+
loop.close()
|
929
|
+
|
930
|
+
def handle_mcp_list_resources(self):
|
931
|
+
"""List available MCP resources."""
|
932
|
+
server_name = request.args.get("server")
|
933
|
+
|
934
|
+
async def get_resources():
|
935
|
+
resources = await self.mcp_client_manager.list_resources(server_name)
|
936
|
+
return [
|
937
|
+
{
|
938
|
+
"uri": resource.uri,
|
939
|
+
"name": resource.name,
|
940
|
+
"description": resource.description,
|
941
|
+
"mimeType": resource.mimeType,
|
942
|
+
"server": resource.metadata.get("server") if resource.metadata else server_name
|
943
|
+
}
|
944
|
+
for resource in resources
|
945
|
+
]
|
946
|
+
|
947
|
+
loop = asyncio.new_event_loop()
|
948
|
+
asyncio.set_event_loop(loop)
|
949
|
+
try:
|
950
|
+
resources = loop.run_until_complete(get_resources())
|
951
|
+
return jsonify({"resources": resources})
|
952
|
+
finally:
|
953
|
+
loop.close()
|
954
|
+
|
955
|
+
def handle_mcp_read_resource(self):
|
956
|
+
"""Read an MCP resource."""
|
957
|
+
data = request.get_json()
|
958
|
+
server_name = data.get("server")
|
959
|
+
uri = data.get("uri")
|
960
|
+
|
961
|
+
if not server_name or not uri:
|
962
|
+
return jsonify({"error": "Missing 'server' or 'uri' parameter"}), 400
|
963
|
+
|
964
|
+
async def read_resource():
|
965
|
+
try:
|
966
|
+
contents = await self.mcp_client_manager.read_resource(server_name, uri)
|
967
|
+
return {
|
968
|
+
"contents": [
|
969
|
+
{"text": content.text} if hasattr(content, 'text') else {"data": str(content)}
|
970
|
+
for content in contents
|
971
|
+
]
|
972
|
+
}
|
973
|
+
except Exception as e:
|
974
|
+
return {"error": str(e)}
|
975
|
+
|
976
|
+
loop = asyncio.new_event_loop()
|
977
|
+
asyncio.set_event_loop(loop)
|
978
|
+
try:
|
979
|
+
result = loop.run_until_complete(read_resource())
|
980
|
+
if "error" in result:
|
981
|
+
return jsonify(result), 500
|
982
|
+
return jsonify(result)
|
983
|
+
finally:
|
984
|
+
loop.close()
|
985
|
+
|
986
|
+
def handle_mcp_server(self):
|
987
|
+
"""Handle MCP server requests using HTTP transport."""
|
988
|
+
if not self.vac_mcp_server:
|
989
|
+
return jsonify({"error": "MCP server not enabled"}), 501
|
990
|
+
|
991
|
+
import json as json_module
|
992
|
+
|
993
|
+
# Handle streaming for HTTP transport
|
994
|
+
if request.method == 'POST':
|
995
|
+
try:
|
996
|
+
# Get the JSON-RPC request
|
997
|
+
data = request.get_json()
|
998
|
+
log.info(f"MCP server received: {data}")
|
999
|
+
|
1000
|
+
# Create an async handler for the request
|
1001
|
+
async def process_request():
|
1002
|
+
# Create mock read/write streams for the server
|
1003
|
+
from io import StringIO
|
1004
|
+
import asyncio
|
1005
|
+
|
1006
|
+
# Convert request to proper format
|
1007
|
+
request_str = json_module.dumps(data) + '\n'
|
1008
|
+
|
1009
|
+
# Create read queue with the request
|
1010
|
+
read_queue = asyncio.Queue()
|
1011
|
+
await read_queue.put(request_str.encode())
|
1012
|
+
await read_queue.put(None) # EOF signal
|
1013
|
+
|
1014
|
+
# Create write queue for response
|
1015
|
+
write_queue = asyncio.Queue()
|
1016
|
+
|
1017
|
+
# Create async iterators
|
1018
|
+
async def read_messages():
|
1019
|
+
while True:
|
1020
|
+
msg = await read_queue.get()
|
1021
|
+
if msg is None:
|
1022
|
+
break
|
1023
|
+
yield msg
|
1024
|
+
|
1025
|
+
responses = []
|
1026
|
+
async def write_messages():
|
1027
|
+
async for msg in write_queue:
|
1028
|
+
if msg is None:
|
1029
|
+
break
|
1030
|
+
responses.append(msg.decode())
|
1031
|
+
|
1032
|
+
# Run the server with these streams
|
1033
|
+
server = self.vac_mcp_server.get_server()
|
1034
|
+
|
1035
|
+
# Start write handler
|
1036
|
+
write_task = asyncio.create_task(write_messages())
|
1037
|
+
|
1038
|
+
try:
|
1039
|
+
# Process the request through the server
|
1040
|
+
await server.run(
|
1041
|
+
read_messages(),
|
1042
|
+
write_queue,
|
1043
|
+
InitializationOptions() if InitializationOptions else None
|
1044
|
+
)
|
1045
|
+
except Exception as e:
|
1046
|
+
log.error(f"Error processing MCP request: {e}")
|
1047
|
+
await write_queue.put(None)
|
1048
|
+
await write_task
|
1049
|
+
raise
|
1050
|
+
|
1051
|
+
# Signal end and wait for write task
|
1052
|
+
await write_queue.put(None)
|
1053
|
+
await write_task
|
1054
|
+
|
1055
|
+
# Return collected responses
|
1056
|
+
return responses
|
1057
|
+
|
1058
|
+
# Run the async handler
|
1059
|
+
loop = asyncio.new_event_loop()
|
1060
|
+
asyncio.set_event_loop(loop)
|
1061
|
+
try:
|
1062
|
+
responses = loop.run_until_complete(process_request())
|
1063
|
+
|
1064
|
+
# Parse and return the response
|
1065
|
+
if responses:
|
1066
|
+
# The response should be a single JSON-RPC response
|
1067
|
+
response_data = json_module.loads(responses[0])
|
1068
|
+
return jsonify(response_data)
|
1069
|
+
else:
|
1070
|
+
return jsonify({"error": "No response from MCP server"}), 500
|
1071
|
+
|
1072
|
+
except Exception as e:
|
1073
|
+
log.error(f"MCP server error: {str(e)}")
|
1074
|
+
return jsonify({
|
1075
|
+
"jsonrpc": "2.0",
|
1076
|
+
"error": {
|
1077
|
+
"code": -32603,
|
1078
|
+
"message": f"Internal error: {str(e)}"
|
1079
|
+
},
|
1080
|
+
"id": data.get("id") if isinstance(data, dict) else None
|
1081
|
+
}), 500
|
1082
|
+
finally:
|
1083
|
+
loop.close()
|
1084
|
+
|
1085
|
+
except Exception as e:
|
1086
|
+
log.error(f"MCP server error: {str(e)}")
|
1087
|
+
return jsonify({
|
1088
|
+
"jsonrpc": "2.0",
|
1089
|
+
"error": {
|
1090
|
+
"code": -32603,
|
1091
|
+
"message": f"Internal error: {str(e)}"
|
1092
|
+
},
|
1093
|
+
"id": data.get("id") if isinstance(data, dict) else None
|
1094
|
+
}), 500
|
1095
|
+
|
1096
|
+
else:
|
1097
|
+
# GET request - return server information
|
1098
|
+
return jsonify({
|
1099
|
+
"name": "sunholo-vac-server",
|
1100
|
+
"version": "1.0.0",
|
1101
|
+
"transport": "http",
|
1102
|
+
"endpoint": "/mcp",
|
1103
|
+
"tools": ["vac_stream", "vac_query"] if self.vac_interpreter else ["vac_stream"]
|
1104
|
+
})
|
sunholo/invoke/async_class.py
CHANGED
@@ -8,12 +8,39 @@ from tenacity import AsyncRetrying, retry_if_exception_type, wait_random_exponen
|
|
8
8
|
log = setup_logging("sunholo_AsyncTaskRunner")
|
9
9
|
|
10
10
|
class AsyncTaskRunner:
|
11
|
-
def __init__(self,
|
11
|
+
def __init__(self,
|
12
|
+
retry_enabled: bool = False,
|
13
|
+
retry_kwargs: dict = None,
|
14
|
+
timeout: int = 120,
|
15
|
+
max_concurrency: int = 20,
|
16
|
+
heartbeat_extends_timeout: bool = False,
|
17
|
+
hard_timeout: int = None):
|
18
|
+
"""
|
19
|
+
Initialize AsyncTaskRunner with configurable timeout behavior.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
retry_enabled: Whether to enable retries
|
23
|
+
retry_kwargs: Retry configuration
|
24
|
+
timeout: Base timeout for tasks (seconds)
|
25
|
+
max_concurrency: Maximum concurrent tasks
|
26
|
+
heartbeat_extends_timeout: If True, heartbeats reset the timeout timer
|
27
|
+
hard_timeout: Maximum absolute timeout regardless of heartbeats (seconds).
|
28
|
+
If None, defaults to timeout * 5 when heartbeat_extends_timeout=True
|
29
|
+
"""
|
12
30
|
self.tasks = []
|
13
31
|
self.retry_enabled = retry_enabled
|
14
32
|
self.retry_kwargs = retry_kwargs or {}
|
15
33
|
self.timeout = timeout
|
16
34
|
self.semaphore = asyncio.Semaphore(max_concurrency)
|
35
|
+
self.heartbeat_extends_timeout = heartbeat_extends_timeout
|
36
|
+
|
37
|
+
# Set hard timeout
|
38
|
+
if hard_timeout is not None:
|
39
|
+
self.hard_timeout = hard_timeout
|
40
|
+
elif heartbeat_extends_timeout:
|
41
|
+
self.hard_timeout = timeout * 5 # Default to 5x base timeout
|
42
|
+
else:
|
43
|
+
self.hard_timeout = timeout # Same as regular timeout
|
17
44
|
|
18
45
|
def add_task(self, func: Callable[..., Any], *args: Any, **kwargs: Any):
|
19
46
|
"""
|
@@ -39,9 +66,11 @@ class AsyncTaskRunner:
|
|
39
66
|
for name, func, args, kwargs in self.tasks:
|
40
67
|
log.info(f"Executing task: {name=}, {func=} with args: {args}, kwargs: {kwargs}")
|
41
68
|
completion_event = asyncio.Event()
|
42
|
-
|
69
|
+
last_heartbeat = {'time': time.time()} # Shared mutable object for heartbeat tracking
|
70
|
+
|
71
|
+
task_coro = self._run_with_retries_and_timeout(name, func, args, kwargs, queue, completion_event, last_heartbeat)
|
43
72
|
task = asyncio.create_task(task_coro)
|
44
|
-
heartbeat_coro = self._send_heartbeat(name, completion_event, queue)
|
73
|
+
heartbeat_coro = self._send_heartbeat(name, completion_event, queue, last_heartbeat)
|
45
74
|
heartbeat_task = asyncio.create_task(heartbeat_coro)
|
46
75
|
task_infos.append({
|
47
76
|
'name': name,
|
@@ -93,9 +122,12 @@ class AsyncTaskRunner:
|
|
93
122
|
args: tuple,
|
94
123
|
kwargs: dict,
|
95
124
|
queue: asyncio.Queue,
|
96
|
-
completion_event: asyncio.Event
|
125
|
+
completion_event: asyncio.Event,
|
126
|
+
last_heartbeat: dict) -> None:
|
97
127
|
try:
|
98
128
|
log.info(f"run_with_retries_and_timeout: {name=}, {func=} with args: {args}, kwargs: {kwargs}")
|
129
|
+
log.info(f"Timeout mode: heartbeat_extends_timeout={self.heartbeat_extends_timeout}, timeout={self.timeout}s, hard_timeout={self.hard_timeout}s")
|
130
|
+
|
99
131
|
if self.retry_enabled:
|
100
132
|
retry_kwargs = {
|
101
133
|
'wait': wait_random_exponential(multiplier=1, max=60),
|
@@ -106,13 +138,13 @@ class AsyncTaskRunner:
|
|
106
138
|
async for attempt in AsyncRetrying(**retry_kwargs):
|
107
139
|
with attempt:
|
108
140
|
log.info(f"Starting task '{name}' with retry")
|
109
|
-
result = await
|
141
|
+
result = await self._execute_task_with_timeout(func, name, last_heartbeat, *args, **kwargs)
|
110
142
|
await queue.put({'type': 'task_complete', 'func_name': name, 'result': result})
|
111
143
|
log.info(f"Sent 'task_complete' message for task '{name}'")
|
112
144
|
return
|
113
145
|
else:
|
114
146
|
log.info(f"Starting task '{name}' with no retry")
|
115
|
-
result = await
|
147
|
+
result = await self._execute_task_with_timeout(func, name, last_heartbeat, *args, **kwargs)
|
116
148
|
await queue.put({'type': 'task_complete', 'func_name': name, 'result': result})
|
117
149
|
log.info(f"Sent 'task_complete' message for task '{name}'")
|
118
150
|
except asyncio.TimeoutError:
|
@@ -125,6 +157,55 @@ class AsyncTaskRunner:
|
|
125
157
|
log.info(f"Task '{name}' completed.")
|
126
158
|
completion_event.set()
|
127
159
|
|
160
|
+
async def _execute_task_with_timeout(self, func: Callable[..., Any], name: str, last_heartbeat: dict, *args: Any, **kwargs: Any) -> Any:
|
161
|
+
"""
|
162
|
+
Execute task with either fixed timeout or heartbeat-extendable timeout.
|
163
|
+
"""
|
164
|
+
if not self.heartbeat_extends_timeout:
|
165
|
+
# Original behavior - fixed timeout
|
166
|
+
return await asyncio.wait_for(self._execute_task(func, *args, **kwargs), timeout=self.timeout)
|
167
|
+
else:
|
168
|
+
# New behavior - heartbeat extends timeout
|
169
|
+
return await self._execute_task_with_heartbeat_timeout(func, name, last_heartbeat, *args, **kwargs)
|
170
|
+
|
171
|
+
async def _execute_task_with_heartbeat_timeout(self, func: Callable[..., Any], name: str, last_heartbeat: dict, *args: Any, **kwargs: Any) -> Any:
|
172
|
+
"""
|
173
|
+
Execute task with heartbeat-extendable timeout and hard timeout limit.
|
174
|
+
"""
|
175
|
+
start_time = time.time()
|
176
|
+
task = asyncio.create_task(self._execute_task(func, *args, **kwargs))
|
177
|
+
|
178
|
+
while not task.done():
|
179
|
+
current_time = time.time()
|
180
|
+
|
181
|
+
# Check hard timeout first (absolute limit)
|
182
|
+
if current_time - start_time > self.hard_timeout:
|
183
|
+
task.cancel()
|
184
|
+
try:
|
185
|
+
await task
|
186
|
+
except asyncio.CancelledError:
|
187
|
+
pass
|
188
|
+
raise asyncio.TimeoutError(f"Hard timeout exceeded ({self.hard_timeout}s)")
|
189
|
+
|
190
|
+
# Check soft timeout (extends with heartbeats)
|
191
|
+
time_since_heartbeat = current_time - last_heartbeat['time']
|
192
|
+
if time_since_heartbeat > self.timeout:
|
193
|
+
task.cancel()
|
194
|
+
try:
|
195
|
+
await task
|
196
|
+
except asyncio.CancelledError:
|
197
|
+
pass
|
198
|
+
raise asyncio.TimeoutError(f"Timeout exceeded - no heartbeat for {self.timeout}s")
|
199
|
+
|
200
|
+
# Wait a bit before checking again
|
201
|
+
try:
|
202
|
+
await asyncio.wait_for(asyncio.shield(task), timeout=1.0)
|
203
|
+
break # Task completed
|
204
|
+
except asyncio.TimeoutError:
|
205
|
+
continue # Check timeouts again
|
206
|
+
|
207
|
+
return await task
|
208
|
+
|
128
209
|
async def _execute_task(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
|
129
210
|
"""
|
130
211
|
Executes the given task function and returns its result.
|
@@ -143,14 +224,16 @@ class AsyncTaskRunner:
|
|
143
224
|
else:
|
144
225
|
return await asyncio.to_thread(func, *args, **kwargs)
|
145
226
|
|
146
|
-
async def _send_heartbeat(self, func_name: str, completion_event: asyncio.Event, queue: asyncio.Queue, interval: int = 2):
|
227
|
+
async def _send_heartbeat(self, func_name: str, completion_event: asyncio.Event, queue: asyncio.Queue, last_heartbeat: dict, interval: int = 2):
|
147
228
|
"""
|
148
229
|
Sends periodic heartbeat updates to indicate the task is still in progress.
|
230
|
+
Updates last_heartbeat time if heartbeat_extends_timeout is enabled.
|
149
231
|
|
150
232
|
Args:
|
151
233
|
func_name (str): The name of the task function.
|
152
234
|
completion_event (asyncio.Event): Event to signal when the task is completed.
|
153
235
|
queue (asyncio.Queue): The queue to send heartbeat messages to.
|
236
|
+
last_heartbeat (dict): Mutable dict containing the last heartbeat time.
|
154
237
|
interval (int): How frequently to send heartbeat messages (in seconds).
|
155
238
|
"""
|
156
239
|
start_time = time.time()
|
@@ -158,7 +241,14 @@ class AsyncTaskRunner:
|
|
158
241
|
try:
|
159
242
|
while not completion_event.is_set():
|
160
243
|
await asyncio.sleep(interval)
|
161
|
-
|
244
|
+
current_time = time.time()
|
245
|
+
elapsed_time = int(current_time - start_time)
|
246
|
+
|
247
|
+
# Update last heartbeat time if heartbeat extends timeout
|
248
|
+
if self.heartbeat_extends_timeout:
|
249
|
+
last_heartbeat['time'] = current_time
|
250
|
+
log.debug(f"Updated heartbeat time for task '{func_name}' at {current_time}")
|
251
|
+
|
162
252
|
heartbeat_message = {
|
163
253
|
'type': 'heartbeat',
|
164
254
|
'name': func_name,
|
sunholo/mcp/__init__.py
CHANGED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Copyright [2024] [Holosun ApS]
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
"""MCP (Model Context Protocol) integration for Sunholo."""
|
16
|
+
|
17
|
+
from .mcp_manager import MCPClientManager
|
18
|
+
from .vac_mcp_server import VACMCPServer
|
19
|
+
|
20
|
+
__all__ = ['MCPClientManager', 'VACMCPServer']
|
@@ -0,0 +1,98 @@
|
|
1
|
+
"""
|
2
|
+
Proper MCP integration for VACRoutes using the official MCP Python SDK.
|
3
|
+
This shows how to integrate MCP servers with your Flask/VACRoutes application.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from typing import Dict, Any, List, Optional
|
7
|
+
|
8
|
+
# Official MCP imports
|
9
|
+
from mcp import StdioServerParameters, ClientSession
|
10
|
+
from mcp.client.stdio import stdio_client
|
11
|
+
from mcp.types import Tool, Resource, TextContent, CallToolResult
|
12
|
+
|
13
|
+
|
14
|
+
class MCPClientManager:
|
15
|
+
"""Manages MCP client connections to various MCP servers."""
|
16
|
+
|
17
|
+
def __init__(self):
|
18
|
+
self.sessions: Dict[str, ClientSession] = {}
|
19
|
+
self.server_configs: Dict[str, Dict[str, Any]] = {}
|
20
|
+
|
21
|
+
async def connect_to_server(self, server_name: str, command: str, args: List[str] = None) -> ClientSession:
|
22
|
+
"""Connect to an MCP server via stdio."""
|
23
|
+
if server_name in self.sessions:
|
24
|
+
return self.sessions[server_name]
|
25
|
+
|
26
|
+
# Create server parameters
|
27
|
+
server_params = StdioServerParameters(
|
28
|
+
command=command,
|
29
|
+
args=args or []
|
30
|
+
)
|
31
|
+
|
32
|
+
# Connect to the server
|
33
|
+
async with stdio_client(server_params) as (read, write):
|
34
|
+
# Create and initialize client session directly
|
35
|
+
session = ClientSession(read, write)
|
36
|
+
await session.initialize()
|
37
|
+
self.sessions[server_name] = session
|
38
|
+
self.server_configs[server_name] = {
|
39
|
+
"command": command,
|
40
|
+
"args": args
|
41
|
+
}
|
42
|
+
return session
|
43
|
+
|
44
|
+
async def list_tools(self, server_name: Optional[str] = None) -> List[Tool]:
|
45
|
+
"""List available tools from one or all connected servers."""
|
46
|
+
if server_name:
|
47
|
+
session = self.sessions.get(server_name)
|
48
|
+
if session:
|
49
|
+
return await session.list_tools()
|
50
|
+
return []
|
51
|
+
|
52
|
+
# List from all servers
|
53
|
+
all_tools = []
|
54
|
+
for name, session in self.sessions.items():
|
55
|
+
tools = await session.list_tools()
|
56
|
+
# Add server name to tool metadata
|
57
|
+
for tool in tools:
|
58
|
+
tool.metadata = tool.metadata or {}
|
59
|
+
tool.metadata["server"] = name
|
60
|
+
all_tools.extend(tools)
|
61
|
+
return all_tools
|
62
|
+
|
63
|
+
async def call_tool(self, server_name: str, tool_name: str, arguments: Dict[str, Any]) -> CallToolResult:
|
64
|
+
"""Call a tool on a specific MCP server."""
|
65
|
+
session = self.sessions.get(server_name)
|
66
|
+
if not session:
|
67
|
+
raise ValueError(f"Not connected to server: {server_name}")
|
68
|
+
|
69
|
+
# Call the tool
|
70
|
+
result = await session.call_tool(tool_name, arguments)
|
71
|
+
return result
|
72
|
+
|
73
|
+
async def list_resources(self, server_name: Optional[str] = None) -> List[Resource]:
|
74
|
+
"""List available resources from servers."""
|
75
|
+
if server_name:
|
76
|
+
session = self.sessions.get(server_name)
|
77
|
+
if session:
|
78
|
+
return await session.list_resources()
|
79
|
+
return []
|
80
|
+
|
81
|
+
# List from all servers
|
82
|
+
all_resources = []
|
83
|
+
for name, session in self.sessions.items():
|
84
|
+
resources = await session.list_resources()
|
85
|
+
for resource in resources:
|
86
|
+
resource.metadata = resource.metadata or {}
|
87
|
+
resource.metadata["server"] = name
|
88
|
+
all_resources.extend(resources)
|
89
|
+
return all_resources
|
90
|
+
|
91
|
+
async def read_resource(self, server_name: str, uri: str) -> List[TextContent]:
|
92
|
+
"""Read a resource from an MCP server."""
|
93
|
+
session = self.sessions.get(server_name)
|
94
|
+
if not session:
|
95
|
+
raise ValueError(f"Not connected to server: {server_name}")
|
96
|
+
|
97
|
+
result = await session.read_resource(uri)
|
98
|
+
return result.contents
|
@@ -0,0 +1,259 @@
|
|
1
|
+
# Copyright [2024] [Holosun ApS]
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
"""
|
16
|
+
MCP Server wrapper for VAC functionality.
|
17
|
+
This module exposes VAC streaming capabilities as MCP tools.
|
18
|
+
"""
|
19
|
+
|
20
|
+
from typing import Any, Sequence, Dict, List, Optional, Callable
|
21
|
+
import json
|
22
|
+
import asyncio
|
23
|
+
from functools import partial
|
24
|
+
|
25
|
+
try:
|
26
|
+
from mcp.server import Server
|
27
|
+
from mcp.types import Tool, TextContent, ImageContent, EmbeddedResource
|
28
|
+
except ImportError:
|
29
|
+
Server = None
|
30
|
+
Tool = None
|
31
|
+
TextContent = None
|
32
|
+
|
33
|
+
from ..custom_logging import log
|
34
|
+
from ..streaming import start_streaming_chat_async
|
35
|
+
|
36
|
+
|
37
|
+
class VACMCPServer:
|
38
|
+
"""MCP Server that exposes VAC functionality as tools."""
|
39
|
+
|
40
|
+
def __init__(self, stream_interpreter: Callable, vac_interpreter: Callable = None):
|
41
|
+
"""
|
42
|
+
Initialize the VAC MCP Server.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
stream_interpreter: The streaming interpreter function
|
46
|
+
vac_interpreter: The static VAC interpreter function (optional)
|
47
|
+
"""
|
48
|
+
if Server is None:
|
49
|
+
raise ImportError("MCP server requires `pip install sunholo[anthropic]`")
|
50
|
+
|
51
|
+
self.stream_interpreter = stream_interpreter
|
52
|
+
self.vac_interpreter = vac_interpreter
|
53
|
+
self.server = Server("sunholo-vac-server")
|
54
|
+
|
55
|
+
# Set up handlers
|
56
|
+
self._setup_handlers()
|
57
|
+
|
58
|
+
def _setup_handlers(self):
|
59
|
+
"""Set up MCP protocol handlers."""
|
60
|
+
|
61
|
+
@self.server.list_tools()
|
62
|
+
async def list_tools() -> List[Tool]:
|
63
|
+
"""List available VAC tools."""
|
64
|
+
tools = [
|
65
|
+
Tool(
|
66
|
+
name="vac_stream",
|
67
|
+
description="Stream responses from a Sunholo VAC (Virtual Agent Computer)",
|
68
|
+
inputSchema={
|
69
|
+
"type": "object",
|
70
|
+
"properties": {
|
71
|
+
"vector_name": {
|
72
|
+
"type": "string",
|
73
|
+
"description": "Name of the VAC to interact with"
|
74
|
+
},
|
75
|
+
"user_input": {
|
76
|
+
"type": "string",
|
77
|
+
"description": "The user's question or input"
|
78
|
+
},
|
79
|
+
"chat_history": {
|
80
|
+
"type": "array",
|
81
|
+
"description": "Previous conversation history",
|
82
|
+
"items": {
|
83
|
+
"type": "object",
|
84
|
+
"properties": {
|
85
|
+
"human": {"type": "string"},
|
86
|
+
"ai": {"type": "string"}
|
87
|
+
}
|
88
|
+
},
|
89
|
+
"default": []
|
90
|
+
},
|
91
|
+
"stream_wait_time": {
|
92
|
+
"type": "number",
|
93
|
+
"description": "Time to wait between stream chunks",
|
94
|
+
"default": 7
|
95
|
+
},
|
96
|
+
"stream_timeout": {
|
97
|
+
"type": "number",
|
98
|
+
"description": "Maximum time to wait for response",
|
99
|
+
"default": 120
|
100
|
+
}
|
101
|
+
},
|
102
|
+
"required": ["vector_name", "user_input"]
|
103
|
+
}
|
104
|
+
)
|
105
|
+
]
|
106
|
+
|
107
|
+
# Add static VAC tool if interpreter is provided
|
108
|
+
if self.vac_interpreter:
|
109
|
+
tools.append(
|
110
|
+
Tool(
|
111
|
+
name="vac_query",
|
112
|
+
description="Query a Sunholo VAC (non-streaming)",
|
113
|
+
inputSchema={
|
114
|
+
"type": "object",
|
115
|
+
"properties": {
|
116
|
+
"vector_name": {
|
117
|
+
"type": "string",
|
118
|
+
"description": "Name of the VAC to interact with"
|
119
|
+
},
|
120
|
+
"user_input": {
|
121
|
+
"type": "string",
|
122
|
+
"description": "The user's question or input"
|
123
|
+
},
|
124
|
+
"chat_history": {
|
125
|
+
"type": "array",
|
126
|
+
"description": "Previous conversation history",
|
127
|
+
"items": {
|
128
|
+
"type": "object",
|
129
|
+
"properties": {
|
130
|
+
"human": {"type": "string"},
|
131
|
+
"ai": {"type": "string"}
|
132
|
+
}
|
133
|
+
},
|
134
|
+
"default": []
|
135
|
+
}
|
136
|
+
},
|
137
|
+
"required": ["vector_name", "user_input"]
|
138
|
+
}
|
139
|
+
)
|
140
|
+
)
|
141
|
+
|
142
|
+
return tools
|
143
|
+
|
144
|
+
@self.server.call_tool()
|
145
|
+
async def call_tool(
|
146
|
+
name: str,
|
147
|
+
arguments: Any
|
148
|
+
) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
|
149
|
+
"""Handle tool calls for VAC interactions."""
|
150
|
+
|
151
|
+
if name == "vac_stream":
|
152
|
+
return await self._handle_vac_stream(arguments)
|
153
|
+
elif name == "vac_query" and self.vac_interpreter:
|
154
|
+
return await self._handle_vac_query(arguments)
|
155
|
+
else:
|
156
|
+
raise ValueError(f"Unknown tool: {name}")
|
157
|
+
|
158
|
+
async def _handle_vac_stream(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
|
159
|
+
"""Handle streaming VAC requests."""
|
160
|
+
vector_name = arguments.get("vector_name")
|
161
|
+
user_input = arguments.get("user_input")
|
162
|
+
chat_history = arguments.get("chat_history", [])
|
163
|
+
stream_wait_time = arguments.get("stream_wait_time", 7)
|
164
|
+
stream_timeout = arguments.get("stream_timeout", 120)
|
165
|
+
|
166
|
+
if not vector_name or not user_input:
|
167
|
+
raise ValueError("Missing required arguments: vector_name and user_input")
|
168
|
+
|
169
|
+
log.info(f"MCP streaming request for VAC '{vector_name}': {user_input}")
|
170
|
+
|
171
|
+
try:
|
172
|
+
# Collect streaming responses
|
173
|
+
full_response = ""
|
174
|
+
|
175
|
+
async for chunk in start_streaming_chat_async(
|
176
|
+
question=user_input,
|
177
|
+
vector_name=vector_name,
|
178
|
+
qna_func_async=self.stream_interpreter,
|
179
|
+
chat_history=chat_history,
|
180
|
+
wait_time=stream_wait_time,
|
181
|
+
timeout=stream_timeout
|
182
|
+
):
|
183
|
+
if isinstance(chunk, dict) and 'answer' in chunk:
|
184
|
+
full_response = chunk['answer']
|
185
|
+
elif isinstance(chunk, str):
|
186
|
+
full_response += chunk
|
187
|
+
|
188
|
+
return [
|
189
|
+
TextContent(
|
190
|
+
type="text",
|
191
|
+
text=full_response or "No response generated"
|
192
|
+
)
|
193
|
+
]
|
194
|
+
|
195
|
+
except Exception as e:
|
196
|
+
log.error(f"Error in MCP VAC stream: {str(e)}")
|
197
|
+
return [
|
198
|
+
TextContent(
|
199
|
+
type="text",
|
200
|
+
text=f"Error: {str(e)}"
|
201
|
+
)
|
202
|
+
]
|
203
|
+
|
204
|
+
async def _handle_vac_query(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
|
205
|
+
"""Handle non-streaming VAC requests."""
|
206
|
+
vector_name = arguments.get("vector_name")
|
207
|
+
user_input = arguments.get("user_input")
|
208
|
+
chat_history = arguments.get("chat_history", [])
|
209
|
+
|
210
|
+
if not vector_name or not user_input:
|
211
|
+
raise ValueError("Missing required arguments: vector_name and user_input")
|
212
|
+
|
213
|
+
log.info(f"MCP query request for VAC '{vector_name}': {user_input}")
|
214
|
+
|
215
|
+
try:
|
216
|
+
# Run in executor if not async
|
217
|
+
if asyncio.iscoroutinefunction(self.vac_interpreter):
|
218
|
+
result = await self.vac_interpreter(
|
219
|
+
question=user_input,
|
220
|
+
vector_name=vector_name,
|
221
|
+
chat_history=chat_history
|
222
|
+
)
|
223
|
+
else:
|
224
|
+
loop = asyncio.get_event_loop()
|
225
|
+
result = await loop.run_in_executor(
|
226
|
+
None,
|
227
|
+
partial(
|
228
|
+
self.vac_interpreter,
|
229
|
+
question=user_input,
|
230
|
+
vector_name=vector_name,
|
231
|
+
chat_history=chat_history
|
232
|
+
)
|
233
|
+
)
|
234
|
+
|
235
|
+
# Extract answer from result
|
236
|
+
if isinstance(result, dict):
|
237
|
+
answer = result.get("answer", str(result))
|
238
|
+
else:
|
239
|
+
answer = str(result)
|
240
|
+
|
241
|
+
return [
|
242
|
+
TextContent(
|
243
|
+
type="text",
|
244
|
+
text=answer
|
245
|
+
)
|
246
|
+
]
|
247
|
+
|
248
|
+
except Exception as e:
|
249
|
+
log.error(f"Error in MCP VAC query: {str(e)}")
|
250
|
+
return [
|
251
|
+
TextContent(
|
252
|
+
type="text",
|
253
|
+
text=f"Error: {str(e)}"
|
254
|
+
)
|
255
|
+
]
|
256
|
+
|
257
|
+
def get_server(self) -> Server:
|
258
|
+
"""Get the underlying MCP server instance."""
|
259
|
+
return self.server
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: sunholo
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.143.1
|
4
4
|
Summary: AI DevOps - a package to help deploy GenAI to the Cloud.
|
5
5
|
Author-email: Holosun ApS <multivac@sunholo.com>
|
6
6
|
License: Apache License, Version 2.0
|
@@ -19,10 +19,11 @@ Requires-Python: >=3.10
|
|
19
19
|
Description-Content-Type: text/markdown
|
20
20
|
License-File: LICENSE.txt
|
21
21
|
Requires-Dist: aiohttp
|
22
|
+
Requires-Dist: flask>=3.1.0
|
22
23
|
Requires-Dist: google-auth
|
23
|
-
Requires-Dist:
|
24
|
-
Requires-Dist: pillow>=11.0.0
|
24
|
+
Requires-Dist: mcp>=1.1.1
|
25
25
|
Requires-Dist: pydantic
|
26
|
+
Requires-Dist: pytest-asyncio>=1.0.0
|
26
27
|
Requires-Dist: requests
|
27
28
|
Requires-Dist: ruamel.yaml
|
28
29
|
Requires-Dist: tenacity
|
@@ -69,7 +70,7 @@ Requires-Dist: langchain-anthropic>=0.1.23; extra == "all"
|
|
69
70
|
Requires-Dist: langchain-google-vertexai; extra == "all"
|
70
71
|
Requires-Dist: langchain-unstructured; extra == "all"
|
71
72
|
Requires-Dist: langfuse; extra == "all"
|
72
|
-
Requires-Dist: mcp; extra == "all"
|
73
|
+
Requires-Dist: mcp>=1.1.1; extra == "all"
|
73
74
|
Requires-Dist: numpy; extra == "all"
|
74
75
|
Requires-Dist: opencv-python; extra == "all"
|
75
76
|
Requires-Dist: pg8000; extra == "all"
|
@@ -155,7 +156,7 @@ Requires-Dist: langchain-openai>=0.3.2; extra == "openai"
|
|
155
156
|
Requires-Dist: tiktoken; extra == "openai"
|
156
157
|
Provides-Extra: anthropic
|
157
158
|
Requires-Dist: langchain-anthropic>=0.1.23; extra == "anthropic"
|
158
|
-
Requires-Dist: mcp; extra == "anthropic"
|
159
|
+
Requires-Dist: mcp>=1.1.1; extra == "anthropic"
|
159
160
|
Provides-Extra: tools
|
160
161
|
Requires-Dist: openapi-spec-validator; extra == "tools"
|
161
162
|
Requires-Dist: playwright; extra == "tools"
|
@@ -14,7 +14,7 @@ sunholo/agents/fastapi/base.py,sha256=W-cyF8ZDUH40rc-c-Apw3-_8IIi2e4Y9qRtnoVnsc1
|
|
14
14
|
sunholo/agents/fastapi/qna_routes.py,sha256=lKHkXPmwltu9EH3RMwmD153-J6pE7kWQ4BhBlV3to-s,3864
|
15
15
|
sunholo/agents/flask/__init__.py,sha256=dEoByI3gDNUOjpX1uVKP7uPjhfFHJubbiaAv3xLopnk,63
|
16
16
|
sunholo/agents/flask/base.py,sha256=vnpxFEOnCmt9humqj-jYPLfJcdwzsop9NorgkJ-tSaU,1756
|
17
|
-
sunholo/agents/flask/vac_routes.py,sha256=
|
17
|
+
sunholo/agents/flask/vac_routes.py,sha256=P_swGAAEee2Z9BdOTQEFza9aI78ezCVtF1MNNi67pug,44599
|
18
18
|
sunholo/archive/__init__.py,sha256=qNHWm5rGPVOlxZBZCpA1wTYPbalizRT7f8X4rs2t290,31
|
19
19
|
sunholo/archive/archive.py,sha256=PxVfDtO2_2ZEEbnhXSCbXLdeoHoQVImo4y3Jr2XkCFY,1204
|
20
20
|
sunholo/auth/__init__.py,sha256=TeP-OY0XGxYV_8AQcVGoh35bvyWhNUcMRfhuD5l44Sk,91
|
@@ -96,7 +96,7 @@ sunholo/genai/init.py,sha256=yG8E67TduFCTQPELo83OJuWfjwTnGZsyACospahyEaY,687
|
|
96
96
|
sunholo/genai/process_funcs_cls.py,sha256=D6eNrc3vtTZzwdkacZNOSfit499N_o0C5AHspyUJiYE,33690
|
97
97
|
sunholo/genai/safety.py,sha256=mkFDO_BeEgiKjQd9o2I4UxB6XI7a9U-oOFjZ8LGRUC4,1238
|
98
98
|
sunholo/invoke/__init__.py,sha256=o1RhwBGOtVK0MIdD55fAIMCkJsxTksi8GD5uoqVKI-8,184
|
99
|
-
sunholo/invoke/async_class.py,sha256=
|
99
|
+
sunholo/invoke/async_class.py,sha256=ZMzxKQtelbYibu9Fac7P9OU3GorH8KxawZxSMv5EO9A,12514
|
100
100
|
sunholo/invoke/direct_vac_func.py,sha256=dACx3Zh7uZnuWLIFYiyLoyXUhh5-eUpd2RatDUd9ov8,9753
|
101
101
|
sunholo/invoke/invoke_vac_utils.py,sha256=sJc1edHTHMzMGXjji1N67c3iUaP7BmAL5nj82Qof63M,2053
|
102
102
|
sunholo/langfuse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -110,8 +110,10 @@ sunholo/llamaindex/llamaindex_class.py,sha256=PnpPoc7LpP7xvKIXYu-UvI4ehj67pGhE1E
|
|
110
110
|
sunholo/llamaindex/user_history.py,sha256=ZtkecWuF9ORduyGB8kF8gP66bm9DdvCI-ZiK6Kt-cSE,2265
|
111
111
|
sunholo/lookup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
112
112
|
sunholo/lookup/model_lookup.yaml,sha256=O7o-jP53MLA06C8pI-ILwERShO-xf6z_258wtpZBv6A,739
|
113
|
-
sunholo/mcp/__init__.py,sha256=
|
113
|
+
sunholo/mcp/__init__.py,sha256=GmKB8Z0ecXUzpuzYEOXSI0aaZP7LZJc7yTCMEFdAmi4,791
|
114
114
|
sunholo/mcp/cli.py,sha256=d24nnVzhZYz4AWgTqmN-qjKG4rPbf8RhdmEOHZkBHy8,10570
|
115
|
+
sunholo/mcp/mcp_manager.py,sha256=ooa_2JSNaKIuK9azEV0OaPVf5Mp_tL_DHaYEyFCmgKY,3752
|
116
|
+
sunholo/mcp/vac_mcp_server.py,sha256=fRcerTqp_pTK8AVITpGPhrez_IaDuUDr4WnVUHPv8GM,10114
|
115
117
|
sunholo/ollama/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
116
118
|
sunholo/ollama/ollama_images.py,sha256=H2cpcNu88R4TwyfL_nnqkQhdvBQ2FPCAy4Ok__0yQmo,2351
|
117
119
|
sunholo/pubsub/__init__.py,sha256=DfTEk4zmCfqn6gFxRrqDO0pOrvXTDqH-medpgYO4PGw,117
|
@@ -169,9 +171,9 @@ sunholo/vertex/init.py,sha256=1OQwcPBKZYBTDPdyU7IM4X4OmiXLdsNV30C-fee2scQ,2875
|
|
169
171
|
sunholo/vertex/memory_tools.py,sha256=tBZxqVZ4InTmdBvLlOYwoSEWu4-kGquc-gxDwZCC4FA,7667
|
170
172
|
sunholo/vertex/safety.py,sha256=S9PgQT1O_BQAkcqauWncRJaydiP8Q_Jzmu9gxYfy1VA,2482
|
171
173
|
sunholo/vertex/type_dict_to_json.py,sha256=uTzL4o9tJRao4u-gJOFcACgWGkBOtqACmb6ihvCErL8,4694
|
172
|
-
sunholo-0.
|
173
|
-
sunholo-0.
|
174
|
-
sunholo-0.
|
175
|
-
sunholo-0.
|
176
|
-
sunholo-0.
|
177
|
-
sunholo-0.
|
174
|
+
sunholo-0.143.1.dist-info/licenses/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
|
175
|
+
sunholo-0.143.1.dist-info/METADATA,sha256=bvNatmW9CaH2Fyn2AN8np1IeiLF0MNciSg7gjihkYvI,18338
|
176
|
+
sunholo-0.143.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
177
|
+
sunholo-0.143.1.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
|
178
|
+
sunholo-0.143.1.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
|
179
|
+
sunholo-0.143.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|