sunholo 0.142.0__py3-none-any.whl → 0.143.3__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/a2a/__init__.py +27 -0
- sunholo/a2a/agent_card.py +345 -0
- sunholo/a2a/task_manager.py +480 -0
- sunholo/a2a/vac_a2a_agent.py +383 -0
- sunholo/agents/flask/vac_routes.py +545 -1
- sunholo/mcp/__init__.py +20 -0
- sunholo/mcp/mcp_manager.py +98 -0
- sunholo/mcp/vac_mcp_server.py +259 -0
- {sunholo-0.142.0.dist-info → sunholo-0.143.3.dist-info}/METADATA +9 -6
- {sunholo-0.142.0.dist-info → sunholo-0.143.3.dist-info}/RECORD +14 -8
- {sunholo-0.142.0.dist-info → sunholo-0.143.3.dist-info}/WHEEL +0 -0
- {sunholo-0.142.0.dist-info → sunholo-0.143.3.dist-info}/entry_points.txt +0 -0
- {sunholo-0.142.0.dist-info → sunholo-0.143.3.dist-info}/licenses/LICENSE.txt +0 -0
- {sunholo-0.142.0.dist-info → sunholo-0.143.3.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,26 @@ 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
|
+
try:
|
|
49
|
+
from ...a2a.vac_a2a_agent import VACA2AAgent
|
|
50
|
+
except (ImportError, SyntaxError):
|
|
51
|
+
VACA2AAgent = None
|
|
52
|
+
|
|
53
|
+
|
|
32
54
|
# Cache dictionary to store validated API keys
|
|
33
55
|
api_key_cache = {}
|
|
34
56
|
cache_duration = timedelta(minutes=5) # Cache duration
|
|
@@ -61,11 +83,41 @@ if __name__ == "__main__":
|
|
|
61
83
|
stream_interpreter: callable,
|
|
62
84
|
vac_interpreter:callable=None,
|
|
63
85
|
additional_routes:dict=None,
|
|
86
|
+
mcp_servers: List[Dict[str, Any]] = None,
|
|
64
87
|
async_stream:bool=False,
|
|
65
|
-
add_langfuse_eval:bool=True
|
|
88
|
+
add_langfuse_eval:bool=True,
|
|
89
|
+
enable_mcp_server:bool=False,
|
|
90
|
+
enable_a2a_agent:bool=False,
|
|
91
|
+
a2a_vac_names: List[str] = None):
|
|
66
92
|
self.app = app
|
|
67
93
|
self.stream_interpreter = stream_interpreter
|
|
68
94
|
self.vac_interpreter = vac_interpreter or partial(self.vac_interpreter_default)
|
|
95
|
+
|
|
96
|
+
# MCP client initialization
|
|
97
|
+
self.mcp_servers = mcp_servers or []
|
|
98
|
+
self.mcp_client_manager = MCPClientManager()
|
|
99
|
+
# Initialize MCP connections
|
|
100
|
+
if self.mcp_servers and self.mcp_client_manager:
|
|
101
|
+
asyncio.create_task(self._initialize_mcp_servers())
|
|
102
|
+
|
|
103
|
+
# MCP server initialization
|
|
104
|
+
self.enable_mcp_server = enable_mcp_server
|
|
105
|
+
self.vac_mcp_server = None
|
|
106
|
+
if self.enable_mcp_server and VACMCPServer:
|
|
107
|
+
self.vac_mcp_server = VACMCPServer(
|
|
108
|
+
stream_interpreter=self.stream_interpreter,
|
|
109
|
+
vac_interpreter=self.vac_interpreter
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# A2A agent initialization
|
|
113
|
+
self.enable_a2a_agent = enable_a2a_agent
|
|
114
|
+
self.vac_a2a_agent = None
|
|
115
|
+
self.a2a_vac_names = a2a_vac_names
|
|
116
|
+
if self.enable_a2a_agent and VACA2AAgent:
|
|
117
|
+
# Extract base URL from request context during route handling
|
|
118
|
+
# For now, initialize with placeholder - will be updated in route handlers
|
|
119
|
+
self.vac_a2a_agent = None # Initialized lazily in route handlers
|
|
120
|
+
|
|
69
121
|
self.additional_routes = additional_routes if additional_routes is not None else []
|
|
70
122
|
self.async_stream = async_stream
|
|
71
123
|
self.add_langfuse_eval = add_langfuse_eval
|
|
@@ -129,6 +181,27 @@ if __name__ == "__main__":
|
|
|
129
181
|
# OpenAI compatible endpoint
|
|
130
182
|
self.app.route('/openai/v1/chat/completions', methods=['POST'])(self.handle_openai_compatible_endpoint)
|
|
131
183
|
self.app.route('/openai/v1/chat/completions/<vector_name>', methods=['POST'])(self.handle_openai_compatible_endpoint)
|
|
184
|
+
|
|
185
|
+
# MCP client routes
|
|
186
|
+
if self.mcp_servers:
|
|
187
|
+
self.app.route('/mcp/tools', methods=['GET'])(self.handle_mcp_list_tools)
|
|
188
|
+
self.app.route('/mcp/tools/<server_name>', methods=['GET'])(self.handle_mcp_list_tools)
|
|
189
|
+
self.app.route('/mcp/call', methods=['POST'])(self.handle_mcp_call_tool)
|
|
190
|
+
self.app.route('/mcp/resources', methods=['GET'])(self.handle_mcp_list_resources)
|
|
191
|
+
self.app.route('/mcp/resources/read', methods=['POST'])(self.handle_mcp_read_resource)
|
|
192
|
+
|
|
193
|
+
# MCP server endpoint
|
|
194
|
+
if self.enable_mcp_server and self.vac_mcp_server:
|
|
195
|
+
self.app.route('/mcp', methods=['POST', 'GET'])(self.handle_mcp_server)
|
|
196
|
+
|
|
197
|
+
# A2A agent endpoints
|
|
198
|
+
if self.enable_a2a_agent:
|
|
199
|
+
self.app.route('/.well-known/agent.json', methods=['GET'])(self.handle_a2a_agent_card)
|
|
200
|
+
self.app.route('/a2a/tasks/send', methods=['POST'])(self.handle_a2a_task_send)
|
|
201
|
+
self.app.route('/a2a/tasks/sendSubscribe', methods=['POST'])(self.handle_a2a_task_send_subscribe)
|
|
202
|
+
self.app.route('/a2a/tasks/get', methods=['POST'])(self.handle_a2a_task_get)
|
|
203
|
+
self.app.route('/a2a/tasks/cancel', methods=['POST'])(self.handle_a2a_task_cancel)
|
|
204
|
+
self.app.route('/a2a/tasks/pushNotification/set', methods=['POST'])(self.handle_a2a_push_notification)
|
|
132
205
|
|
|
133
206
|
self.register_additional_routes()
|
|
134
207
|
|
|
@@ -803,3 +876,474 @@ if __name__ == "__main__":
|
|
|
803
876
|
except Exception as e:
|
|
804
877
|
raise Exception(f'File upload failed: {str(e)}')
|
|
805
878
|
|
|
879
|
+
async def _initialize_mcp_servers(self):
|
|
880
|
+
"""Initialize connections to configured MCP servers."""
|
|
881
|
+
for server_config in self.mcp_servers:
|
|
882
|
+
try:
|
|
883
|
+
await self.mcp_client_manager.connect_to_server(
|
|
884
|
+
server_name=server_config["name"],
|
|
885
|
+
command=server_config["command"],
|
|
886
|
+
args=server_config.get("args", [])
|
|
887
|
+
)
|
|
888
|
+
log.info(f"Connected to MCP server: {server_config['name']}")
|
|
889
|
+
except Exception as e:
|
|
890
|
+
log.error(f"Failed to connect to MCP server {server_config['name']}: {e}")
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def handle_mcp_list_tools(self, server_name: Optional[str] = None):
|
|
894
|
+
"""List available MCP tools."""
|
|
895
|
+
async def get_tools():
|
|
896
|
+
tools = await self.mcp_client_manager.list_tools(server_name)
|
|
897
|
+
return [
|
|
898
|
+
{
|
|
899
|
+
"name": tool.name,
|
|
900
|
+
"description": tool.description,
|
|
901
|
+
"inputSchema": tool.inputSchema,
|
|
902
|
+
"server": tool.metadata.get("server") if tool.metadata else server_name
|
|
903
|
+
}
|
|
904
|
+
for tool in tools
|
|
905
|
+
]
|
|
906
|
+
|
|
907
|
+
# Run async in sync context
|
|
908
|
+
loop = asyncio.new_event_loop()
|
|
909
|
+
asyncio.set_event_loop(loop)
|
|
910
|
+
try:
|
|
911
|
+
tools = loop.run_until_complete(get_tools())
|
|
912
|
+
return jsonify({"tools": tools})
|
|
913
|
+
finally:
|
|
914
|
+
loop.close()
|
|
915
|
+
|
|
916
|
+
def handle_mcp_call_tool(self):
|
|
917
|
+
"""Call an MCP tool."""
|
|
918
|
+
data = request.get_json()
|
|
919
|
+
server_name = data.get("server")
|
|
920
|
+
tool_name = data.get("tool")
|
|
921
|
+
arguments = data.get("arguments", {})
|
|
922
|
+
|
|
923
|
+
if not server_name or not tool_name:
|
|
924
|
+
return jsonify({"error": "Missing 'server' or 'tool' parameter"}), 400
|
|
925
|
+
|
|
926
|
+
async def call_tool():
|
|
927
|
+
try:
|
|
928
|
+
result = await self.mcp_client_manager.call_tool(server_name, tool_name, arguments)
|
|
929
|
+
|
|
930
|
+
# Convert result to JSON-serializable format
|
|
931
|
+
if hasattr(result, 'content'):
|
|
932
|
+
# Handle different content types
|
|
933
|
+
if hasattr(result.content, 'text'):
|
|
934
|
+
return {"result": result.content.text}
|
|
935
|
+
elif hasattr(result.content, 'data'):
|
|
936
|
+
return {"result": result.content.data}
|
|
937
|
+
else:
|
|
938
|
+
return {"result": str(result.content)}
|
|
939
|
+
else:
|
|
940
|
+
return {"result": str(result)}
|
|
941
|
+
|
|
942
|
+
except Exception as e:
|
|
943
|
+
return {"error": str(e)}
|
|
944
|
+
|
|
945
|
+
loop = asyncio.new_event_loop()
|
|
946
|
+
asyncio.set_event_loop(loop)
|
|
947
|
+
try:
|
|
948
|
+
result = loop.run_until_complete(call_tool())
|
|
949
|
+
if "error" in result:
|
|
950
|
+
return jsonify(result), 500
|
|
951
|
+
return jsonify(result)
|
|
952
|
+
finally:
|
|
953
|
+
loop.close()
|
|
954
|
+
|
|
955
|
+
def handle_mcp_list_resources(self):
|
|
956
|
+
"""List available MCP resources."""
|
|
957
|
+
server_name = request.args.get("server")
|
|
958
|
+
|
|
959
|
+
async def get_resources():
|
|
960
|
+
resources = await self.mcp_client_manager.list_resources(server_name)
|
|
961
|
+
return [
|
|
962
|
+
{
|
|
963
|
+
"uri": resource.uri,
|
|
964
|
+
"name": resource.name,
|
|
965
|
+
"description": resource.description,
|
|
966
|
+
"mimeType": resource.mimeType,
|
|
967
|
+
"server": resource.metadata.get("server") if resource.metadata else server_name
|
|
968
|
+
}
|
|
969
|
+
for resource in resources
|
|
970
|
+
]
|
|
971
|
+
|
|
972
|
+
loop = asyncio.new_event_loop()
|
|
973
|
+
asyncio.set_event_loop(loop)
|
|
974
|
+
try:
|
|
975
|
+
resources = loop.run_until_complete(get_resources())
|
|
976
|
+
return jsonify({"resources": resources})
|
|
977
|
+
finally:
|
|
978
|
+
loop.close()
|
|
979
|
+
|
|
980
|
+
def handle_mcp_read_resource(self):
|
|
981
|
+
"""Read an MCP resource."""
|
|
982
|
+
data = request.get_json()
|
|
983
|
+
server_name = data.get("server")
|
|
984
|
+
uri = data.get("uri")
|
|
985
|
+
|
|
986
|
+
if not server_name or not uri:
|
|
987
|
+
return jsonify({"error": "Missing 'server' or 'uri' parameter"}), 400
|
|
988
|
+
|
|
989
|
+
async def read_resource():
|
|
990
|
+
try:
|
|
991
|
+
contents = await self.mcp_client_manager.read_resource(server_name, uri)
|
|
992
|
+
return {
|
|
993
|
+
"contents": [
|
|
994
|
+
{"text": content.text} if hasattr(content, 'text') else {"data": str(content)}
|
|
995
|
+
for content in contents
|
|
996
|
+
]
|
|
997
|
+
}
|
|
998
|
+
except Exception as e:
|
|
999
|
+
return {"error": str(e)}
|
|
1000
|
+
|
|
1001
|
+
loop = asyncio.new_event_loop()
|
|
1002
|
+
asyncio.set_event_loop(loop)
|
|
1003
|
+
try:
|
|
1004
|
+
result = loop.run_until_complete(read_resource())
|
|
1005
|
+
if "error" in result:
|
|
1006
|
+
return jsonify(result), 500
|
|
1007
|
+
return jsonify(result)
|
|
1008
|
+
finally:
|
|
1009
|
+
loop.close()
|
|
1010
|
+
|
|
1011
|
+
def handle_mcp_server(self):
|
|
1012
|
+
"""Handle MCP server requests using HTTP transport."""
|
|
1013
|
+
if not self.vac_mcp_server:
|
|
1014
|
+
return jsonify({"error": "MCP server not enabled"}), 501
|
|
1015
|
+
|
|
1016
|
+
import json as json_module
|
|
1017
|
+
|
|
1018
|
+
# Handle streaming for HTTP transport
|
|
1019
|
+
if request.method == 'POST':
|
|
1020
|
+
try:
|
|
1021
|
+
# Get the JSON-RPC request
|
|
1022
|
+
data = request.get_json()
|
|
1023
|
+
log.info(f"MCP server received: {data}")
|
|
1024
|
+
|
|
1025
|
+
# Create an async handler for the request
|
|
1026
|
+
async def process_request():
|
|
1027
|
+
# Create mock read/write streams for the server
|
|
1028
|
+
from io import StringIO
|
|
1029
|
+
import asyncio
|
|
1030
|
+
|
|
1031
|
+
# Convert request to proper format
|
|
1032
|
+
request_str = json_module.dumps(data) + '\n'
|
|
1033
|
+
|
|
1034
|
+
# Create read queue with the request
|
|
1035
|
+
read_queue = asyncio.Queue()
|
|
1036
|
+
await read_queue.put(request_str.encode())
|
|
1037
|
+
await read_queue.put(None) # EOF signal
|
|
1038
|
+
|
|
1039
|
+
# Create write queue for response
|
|
1040
|
+
write_queue = asyncio.Queue()
|
|
1041
|
+
|
|
1042
|
+
# Create async iterators
|
|
1043
|
+
async def read_messages():
|
|
1044
|
+
while True:
|
|
1045
|
+
msg = await read_queue.get()
|
|
1046
|
+
if msg is None:
|
|
1047
|
+
break
|
|
1048
|
+
yield msg
|
|
1049
|
+
|
|
1050
|
+
responses = []
|
|
1051
|
+
async def write_messages():
|
|
1052
|
+
async for msg in write_queue:
|
|
1053
|
+
if msg is None:
|
|
1054
|
+
break
|
|
1055
|
+
responses.append(msg.decode())
|
|
1056
|
+
|
|
1057
|
+
# Run the server with these streams
|
|
1058
|
+
server = self.vac_mcp_server.get_server()
|
|
1059
|
+
|
|
1060
|
+
# Start write handler
|
|
1061
|
+
write_task = asyncio.create_task(write_messages())
|
|
1062
|
+
|
|
1063
|
+
try:
|
|
1064
|
+
# Process the request through the server
|
|
1065
|
+
await server.run(
|
|
1066
|
+
read_messages(),
|
|
1067
|
+
write_queue,
|
|
1068
|
+
InitializationOptions() if InitializationOptions else None
|
|
1069
|
+
)
|
|
1070
|
+
except Exception as e:
|
|
1071
|
+
log.error(f"Error processing MCP request: {e}")
|
|
1072
|
+
await write_queue.put(None)
|
|
1073
|
+
await write_task
|
|
1074
|
+
raise
|
|
1075
|
+
|
|
1076
|
+
# Signal end and wait for write task
|
|
1077
|
+
await write_queue.put(None)
|
|
1078
|
+
await write_task
|
|
1079
|
+
|
|
1080
|
+
# Return collected responses
|
|
1081
|
+
return responses
|
|
1082
|
+
|
|
1083
|
+
# Run the async handler
|
|
1084
|
+
loop = asyncio.new_event_loop()
|
|
1085
|
+
asyncio.set_event_loop(loop)
|
|
1086
|
+
try:
|
|
1087
|
+
responses = loop.run_until_complete(process_request())
|
|
1088
|
+
|
|
1089
|
+
# Parse and return the response
|
|
1090
|
+
if responses:
|
|
1091
|
+
# The response should be a single JSON-RPC response
|
|
1092
|
+
response_data = json_module.loads(responses[0])
|
|
1093
|
+
return jsonify(response_data)
|
|
1094
|
+
else:
|
|
1095
|
+
return jsonify({"error": "No response from MCP server"}), 500
|
|
1096
|
+
|
|
1097
|
+
except Exception as e:
|
|
1098
|
+
log.error(f"MCP server error: {str(e)}")
|
|
1099
|
+
return jsonify({
|
|
1100
|
+
"jsonrpc": "2.0",
|
|
1101
|
+
"error": {
|
|
1102
|
+
"code": -32603,
|
|
1103
|
+
"message": f"Internal error: {str(e)}"
|
|
1104
|
+
},
|
|
1105
|
+
"id": data.get("id") if isinstance(data, dict) else None
|
|
1106
|
+
}), 500
|
|
1107
|
+
finally:
|
|
1108
|
+
loop.close()
|
|
1109
|
+
|
|
1110
|
+
except Exception as e:
|
|
1111
|
+
log.error(f"MCP server error: {str(e)}")
|
|
1112
|
+
return jsonify({
|
|
1113
|
+
"jsonrpc": "2.0",
|
|
1114
|
+
"error": {
|
|
1115
|
+
"code": -32603,
|
|
1116
|
+
"message": f"Internal error: {str(e)}"
|
|
1117
|
+
},
|
|
1118
|
+
"id": data.get("id") if isinstance(data, dict) else None
|
|
1119
|
+
}), 500
|
|
1120
|
+
|
|
1121
|
+
else:
|
|
1122
|
+
# GET request - return server information
|
|
1123
|
+
return jsonify({
|
|
1124
|
+
"name": "sunholo-vac-server",
|
|
1125
|
+
"version": "1.0.0",
|
|
1126
|
+
"transport": "http",
|
|
1127
|
+
"endpoint": "/mcp",
|
|
1128
|
+
"tools": ["vac_stream", "vac_query"] if self.vac_interpreter else ["vac_stream"]
|
|
1129
|
+
})
|
|
1130
|
+
|
|
1131
|
+
def _get_or_create_a2a_agent(self):
|
|
1132
|
+
"""Get or create the A2A agent instance with current request context."""
|
|
1133
|
+
if not self.enable_a2a_agent or not VACA2AAgent:
|
|
1134
|
+
return None
|
|
1135
|
+
|
|
1136
|
+
if self.vac_a2a_agent is None:
|
|
1137
|
+
# Extract base URL from current request
|
|
1138
|
+
base_url = request.url_root.rstrip('/')
|
|
1139
|
+
|
|
1140
|
+
self.vac_a2a_agent = VACA2AAgent(
|
|
1141
|
+
base_url=base_url,
|
|
1142
|
+
stream_interpreter=self.stream_interpreter,
|
|
1143
|
+
vac_interpreter=self.vac_interpreter,
|
|
1144
|
+
vac_names=self.a2a_vac_names
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
return self.vac_a2a_agent
|
|
1148
|
+
|
|
1149
|
+
def handle_a2a_agent_card(self):
|
|
1150
|
+
"""Handle A2A agent card discovery request."""
|
|
1151
|
+
agent = self._get_or_create_a2a_agent()
|
|
1152
|
+
if not agent:
|
|
1153
|
+
return jsonify({"error": "A2A agent not enabled"}), 501
|
|
1154
|
+
|
|
1155
|
+
return jsonify(agent.get_agent_card())
|
|
1156
|
+
|
|
1157
|
+
def handle_a2a_task_send(self):
|
|
1158
|
+
"""Handle A2A task send request."""
|
|
1159
|
+
agent = self._get_or_create_a2a_agent()
|
|
1160
|
+
if not agent:
|
|
1161
|
+
return jsonify({"error": "A2A agent not enabled"}), 501
|
|
1162
|
+
|
|
1163
|
+
try:
|
|
1164
|
+
data = request.get_json()
|
|
1165
|
+
if not data:
|
|
1166
|
+
return jsonify({
|
|
1167
|
+
"jsonrpc": "2.0",
|
|
1168
|
+
"error": {
|
|
1169
|
+
"code": -32700,
|
|
1170
|
+
"message": "Parse error: Invalid JSON"
|
|
1171
|
+
},
|
|
1172
|
+
"id": None
|
|
1173
|
+
}), 400
|
|
1174
|
+
|
|
1175
|
+
# Run async handler
|
|
1176
|
+
loop = asyncio.new_event_loop()
|
|
1177
|
+
asyncio.set_event_loop(loop)
|
|
1178
|
+
try:
|
|
1179
|
+
response = loop.run_until_complete(agent.handle_task_send(data))
|
|
1180
|
+
return jsonify(response)
|
|
1181
|
+
finally:
|
|
1182
|
+
loop.close()
|
|
1183
|
+
|
|
1184
|
+
except Exception as e:
|
|
1185
|
+
log.error(f"A2A task send error: {e}")
|
|
1186
|
+
return jsonify({
|
|
1187
|
+
"jsonrpc": "2.0",
|
|
1188
|
+
"error": {
|
|
1189
|
+
"code": -32603,
|
|
1190
|
+
"message": f"Internal error: {str(e)}"
|
|
1191
|
+
},
|
|
1192
|
+
"id": data.get("id") if 'data' in locals() else None
|
|
1193
|
+
}), 500
|
|
1194
|
+
|
|
1195
|
+
def handle_a2a_task_send_subscribe(self):
|
|
1196
|
+
"""Handle A2A task send with subscription (SSE)."""
|
|
1197
|
+
agent = self._get_or_create_a2a_agent()
|
|
1198
|
+
if not agent:
|
|
1199
|
+
return jsonify({"error": "A2A agent not enabled"}), 501
|
|
1200
|
+
|
|
1201
|
+
try:
|
|
1202
|
+
data = request.get_json()
|
|
1203
|
+
if not data:
|
|
1204
|
+
def error_generator():
|
|
1205
|
+
yield "data: {\"error\": \"Parse error: Invalid JSON\"}\n\n"
|
|
1206
|
+
|
|
1207
|
+
return Response(error_generator(), content_type='text/event-stream')
|
|
1208
|
+
|
|
1209
|
+
# Create async generator for SSE
|
|
1210
|
+
async def sse_generator():
|
|
1211
|
+
async for chunk in agent.handle_task_send_subscribe(data):
|
|
1212
|
+
yield chunk
|
|
1213
|
+
|
|
1214
|
+
def sync_generator():
|
|
1215
|
+
loop = asyncio.new_event_loop()
|
|
1216
|
+
asyncio.set_event_loop(loop)
|
|
1217
|
+
try:
|
|
1218
|
+
async_gen = sse_generator()
|
|
1219
|
+
while True:
|
|
1220
|
+
try:
|
|
1221
|
+
chunk = loop.run_until_complete(async_gen.__anext__())
|
|
1222
|
+
yield chunk
|
|
1223
|
+
except StopAsyncIteration:
|
|
1224
|
+
break
|
|
1225
|
+
finally:
|
|
1226
|
+
loop.close()
|
|
1227
|
+
|
|
1228
|
+
return Response(sync_generator(), content_type='text/event-stream')
|
|
1229
|
+
|
|
1230
|
+
except Exception as e:
|
|
1231
|
+
log.error(f"A2A task send subscribe error: {e}")
|
|
1232
|
+
def error_generator(err):
|
|
1233
|
+
yield f"data: {{\"error\": \"Internal error: {str(err)}\"}}\n\n"
|
|
1234
|
+
|
|
1235
|
+
return Response(error_generator(e), content_type='text/event-stream')
|
|
1236
|
+
|
|
1237
|
+
def handle_a2a_task_get(self):
|
|
1238
|
+
"""Handle A2A task get request."""
|
|
1239
|
+
agent = self._get_or_create_a2a_agent()
|
|
1240
|
+
if not agent:
|
|
1241
|
+
return jsonify({"error": "A2A agent not enabled"}), 501
|
|
1242
|
+
|
|
1243
|
+
try:
|
|
1244
|
+
data = request.get_json()
|
|
1245
|
+
if not data:
|
|
1246
|
+
return jsonify({
|
|
1247
|
+
"jsonrpc": "2.0",
|
|
1248
|
+
"error": {
|
|
1249
|
+
"code": -32700,
|
|
1250
|
+
"message": "Parse error: Invalid JSON"
|
|
1251
|
+
},
|
|
1252
|
+
"id": None
|
|
1253
|
+
}), 400
|
|
1254
|
+
|
|
1255
|
+
# Run async handler
|
|
1256
|
+
loop = asyncio.new_event_loop()
|
|
1257
|
+
asyncio.set_event_loop(loop)
|
|
1258
|
+
try:
|
|
1259
|
+
response = loop.run_until_complete(agent.handle_task_get(data))
|
|
1260
|
+
return jsonify(response)
|
|
1261
|
+
finally:
|
|
1262
|
+
loop.close()
|
|
1263
|
+
|
|
1264
|
+
except Exception as e:
|
|
1265
|
+
log.error(f"A2A task get error: {e}")
|
|
1266
|
+
return jsonify({
|
|
1267
|
+
"jsonrpc": "2.0",
|
|
1268
|
+
"error": {
|
|
1269
|
+
"code": -32603,
|
|
1270
|
+
"message": f"Internal error: {str(e)}"
|
|
1271
|
+
},
|
|
1272
|
+
"id": data.get("id") if 'data' in locals() else None
|
|
1273
|
+
}), 500
|
|
1274
|
+
|
|
1275
|
+
def handle_a2a_task_cancel(self):
|
|
1276
|
+
"""Handle A2A task cancel request."""
|
|
1277
|
+
agent = self._get_or_create_a2a_agent()
|
|
1278
|
+
if not agent:
|
|
1279
|
+
return jsonify({"error": "A2A agent not enabled"}), 501
|
|
1280
|
+
|
|
1281
|
+
try:
|
|
1282
|
+
data = request.get_json()
|
|
1283
|
+
if not data:
|
|
1284
|
+
return jsonify({
|
|
1285
|
+
"jsonrpc": "2.0",
|
|
1286
|
+
"error": {
|
|
1287
|
+
"code": -32700,
|
|
1288
|
+
"message": "Parse error: Invalid JSON"
|
|
1289
|
+
},
|
|
1290
|
+
"id": None
|
|
1291
|
+
}), 400
|
|
1292
|
+
|
|
1293
|
+
# Run async handler
|
|
1294
|
+
loop = asyncio.new_event_loop()
|
|
1295
|
+
asyncio.set_event_loop(loop)
|
|
1296
|
+
try:
|
|
1297
|
+
response = loop.run_until_complete(agent.handle_task_cancel(data))
|
|
1298
|
+
return jsonify(response)
|
|
1299
|
+
finally:
|
|
1300
|
+
loop.close()
|
|
1301
|
+
|
|
1302
|
+
except Exception as e:
|
|
1303
|
+
log.error(f"A2A task cancel error: {e}")
|
|
1304
|
+
return jsonify({
|
|
1305
|
+
"jsonrpc": "2.0",
|
|
1306
|
+
"error": {
|
|
1307
|
+
"code": -32603,
|
|
1308
|
+
"message": f"Internal error: {str(e)}"
|
|
1309
|
+
},
|
|
1310
|
+
"id": data.get("id") if 'data' in locals() else None
|
|
1311
|
+
}), 500
|
|
1312
|
+
|
|
1313
|
+
def handle_a2a_push_notification(self):
|
|
1314
|
+
"""Handle A2A push notification settings."""
|
|
1315
|
+
agent = self._get_or_create_a2a_agent()
|
|
1316
|
+
if not agent:
|
|
1317
|
+
return jsonify({"error": "A2A agent not enabled"}), 501
|
|
1318
|
+
|
|
1319
|
+
try:
|
|
1320
|
+
data = request.get_json()
|
|
1321
|
+
if not data:
|
|
1322
|
+
return jsonify({
|
|
1323
|
+
"jsonrpc": "2.0",
|
|
1324
|
+
"error": {
|
|
1325
|
+
"code": -32700,
|
|
1326
|
+
"message": "Parse error: Invalid JSON"
|
|
1327
|
+
},
|
|
1328
|
+
"id": None
|
|
1329
|
+
}), 400
|
|
1330
|
+
|
|
1331
|
+
# Run async handler
|
|
1332
|
+
loop = asyncio.new_event_loop()
|
|
1333
|
+
asyncio.set_event_loop(loop)
|
|
1334
|
+
try:
|
|
1335
|
+
response = loop.run_until_complete(agent.handle_push_notification_set(data))
|
|
1336
|
+
return jsonify(response)
|
|
1337
|
+
finally:
|
|
1338
|
+
loop.close()
|
|
1339
|
+
|
|
1340
|
+
except Exception as e:
|
|
1341
|
+
log.error(f"A2A push notification error: {e}")
|
|
1342
|
+
return jsonify({
|
|
1343
|
+
"jsonrpc": "2.0",
|
|
1344
|
+
"error": {
|
|
1345
|
+
"code": -32603,
|
|
1346
|
+
"message": f"Internal error: {str(e)}"
|
|
1347
|
+
},
|
|
1348
|
+
"id": data.get("id") if 'data' in locals() else None
|
|
1349
|
+
}), 500
|
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']
|