libentry 1.23.3__py3-none-any.whl → 1.24__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.
- libentry/mcp/api.py +8 -2
- libentry/mcp/service.py +104 -36
- libentry/resource.py +78 -0
- {libentry-1.23.3.dist-info → libentry-1.24.dist-info}/METADATA +1 -1
- {libentry-1.23.3.dist-info → libentry-1.24.dist-info}/RECORD +10 -9
- {libentry-1.23.3.dist-info → libentry-1.24.dist-info}/LICENSE +0 -0
- {libentry-1.23.3.dist-info → libentry-1.24.dist-info}/WHEEL +0 -0
- {libentry-1.23.3.dist-info → libentry-1.24.dist-info}/entry_points.txt +0 -0
- {libentry-1.23.3.dist-info → libentry-1.24.dist-info}/top_level.txt +0 -0
- {libentry-1.23.3.dist-info → libentry-1.24.dist-info}/zip-safe +0 -0
libentry/mcp/api.py
CHANGED
@@ -19,6 +19,12 @@ from pydantic import BaseModel, ConfigDict
|
|
19
19
|
|
20
20
|
API_INFO_FIELD = "__api_info__"
|
21
21
|
|
22
|
+
TAG_SUBROUTINE = "subroutine"
|
23
|
+
TAG_JSONRPC = "jsonrpc"
|
24
|
+
TAG_ENDPOINT = "endpoint"
|
25
|
+
TAG_TOOL = "tool"
|
26
|
+
TAG_RESOURCE = "resource"
|
27
|
+
|
22
28
|
|
23
29
|
class APIInfo(BaseModel):
|
24
30
|
path: str
|
@@ -69,7 +75,7 @@ def tool(path=None, name=None, description=None, **kwargs):
|
|
69
75
|
methods=["POST"],
|
70
76
|
name=name,
|
71
77
|
description=description,
|
72
|
-
tag=
|
78
|
+
tag=TAG_TOOL,
|
73
79
|
**kwargs
|
74
80
|
)
|
75
81
|
|
@@ -80,7 +86,7 @@ def resource(uri, name=None, description=None, mime_type=None, size=None, **kwar
|
|
80
86
|
methods=["POST"],
|
81
87
|
name=name,
|
82
88
|
description=description,
|
83
|
-
tag=
|
89
|
+
tag=TAG_RESOURCE,
|
84
90
|
mimeType=mime_type,
|
85
91
|
size=size,
|
86
92
|
uri=uri,
|
libentry/mcp/service.py
CHANGED
@@ -57,11 +57,24 @@ class SubroutineAdapter:
|
|
57
57
|
|
58
58
|
def __call__(
|
59
59
|
self,
|
60
|
-
request:
|
60
|
+
request: Any,
|
61
|
+
args: Optional[Dict[str, str]] = None
|
61
62
|
) -> Union[SubroutineResponse, Iterable[SubroutineResponse]]:
|
62
63
|
if isinstance(request, BaseModel):
|
63
64
|
request = request.model_dump()
|
64
65
|
|
66
|
+
if not isinstance(request, Dict):
|
67
|
+
raise ValueError(
|
68
|
+
f"Subroutine only accepts JSON object as input. "
|
69
|
+
f"Expect \"dict\", got \"{type(request)}\"."
|
70
|
+
)
|
71
|
+
|
72
|
+
if args is not None:
|
73
|
+
conflicts = args.keys() & request.keys()
|
74
|
+
if len(conflicts) > 0:
|
75
|
+
raise ValueError(f"Duplicated fields: \"{conflicts}\".")
|
76
|
+
request = {**args, **request}
|
77
|
+
|
65
78
|
try:
|
66
79
|
input_model = self.api_signature.input_model
|
67
80
|
if input_model is not None:
|
@@ -117,16 +130,29 @@ class JSONRPCAdapter:
|
|
117
130
|
|
118
131
|
def __call__(
|
119
132
|
self,
|
120
|
-
request: Union[JSONRPCRequest, JSONRPCNotification, Dict[str, Any]]
|
133
|
+
request: Union[JSONRPCRequest, JSONRPCNotification, Dict[str, Any]],
|
134
|
+
args: Optional[Dict[str, str]] = None
|
121
135
|
) -> Union[JSONRPCResponse, Iterable[JSONRPCResponse], None]:
|
122
136
|
if isinstance(request, Dict):
|
137
|
+
if args is not None:
|
138
|
+
conflicts = args.keys() & request.keys()
|
139
|
+
if len(conflicts) > 0:
|
140
|
+
raise ValueError(f"Duplicated fields: \"{conflicts}\".")
|
141
|
+
request = {**args, **request}
|
123
142
|
request = self.type_adapter.validate_python(request)
|
124
143
|
|
144
|
+
if isinstance(request, JSONRPCRequest):
|
145
|
+
fn = self._apply_request
|
146
|
+
elif isinstance(request, JSONRPCNotification):
|
147
|
+
fn = self._apply_notification
|
148
|
+
else:
|
149
|
+
raise ValueError(
|
150
|
+
f"JSONRPC only accepts JSONRPCRequest, JSONRPCNotification as input. "
|
151
|
+
f"Expect \"JSONRPCRequest\", \"JSONRPCNotification\" or \"dict\", got \"{type(request)}\"."
|
152
|
+
)
|
153
|
+
|
125
154
|
try:
|
126
|
-
|
127
|
-
return self._apply_request(request)
|
128
|
-
else:
|
129
|
-
return self._apply_notification(request)
|
155
|
+
return fn(request)
|
130
156
|
except SystemExit as e:
|
131
157
|
raise e
|
132
158
|
except KeyboardInterrupt as e:
|
@@ -233,44 +259,38 @@ class FlaskHandler:
|
|
233
259
|
|
234
260
|
self.subroutine_adapter = SubroutineAdapter(fn, self.api_signature)
|
235
261
|
self.jsonrpc_adapter = JSONRPCAdapter(fn, self.api_signature)
|
236
|
-
self.default_adapter = self.subroutine_adapter
|
237
262
|
|
263
|
+
adapter_mapping = {
|
264
|
+
api.TAG_ENDPOINT: self.fn,
|
265
|
+
"free": self.fn,
|
266
|
+
"schema_free": self.fn,
|
267
|
+
"schema-free": self.fn,
|
268
|
+
api.TAG_JSONRPC: self.jsonrpc_adapter,
|
269
|
+
"rpc": self.jsonrpc_adapter,
|
270
|
+
"mcp": self.jsonrpc_adapter,
|
271
|
+
}
|
238
272
|
tag = self.api_info.tag if self.api_info else None
|
239
|
-
|
240
|
-
tag = tag.lower()
|
241
|
-
if tag in {"free", "schema_free", "schema-free"}:
|
242
|
-
self.default_adapter = self.fn
|
243
|
-
elif tag in {"jsonrpc", "rpc"}:
|
244
|
-
self.default_adapter = self.jsonrpc_adapter
|
245
|
-
|
246
|
-
# todo: debug info
|
247
|
-
print(f"{api_info.path}: {type(self.default_adapter)}\t{self.api_signature.input_model}")
|
273
|
+
self.default_adapter = adapter_mapping.get(tag, self.subroutine_adapter)
|
248
274
|
|
249
275
|
def __call__(self):
|
250
276
|
args = flask_request.args
|
251
277
|
data = flask_request.data
|
252
278
|
content_type = flask_request.content_type
|
253
279
|
|
254
|
-
|
280
|
+
args = {**args}
|
255
281
|
if data:
|
256
282
|
if (not content_type) or content_type == MIME.json.value:
|
257
|
-
|
283
|
+
raw_request = json.loads(data)
|
258
284
|
else:
|
259
285
|
return self.app.error(f"Unsupported Content-Type: \"{content_type}\".")
|
260
286
|
else:
|
261
|
-
|
262
|
-
|
263
|
-
conflicts = json_from_url.keys() & json_from_data.keys()
|
264
|
-
if len(conflicts) > 0:
|
265
|
-
return self.app.error(f"Duplicated fields: \"{conflicts}\".")
|
266
|
-
|
267
|
-
input_json = {**json_from_url, **json_from_data}
|
287
|
+
raw_request = {}
|
268
288
|
|
269
289
|
################################################################################
|
270
290
|
# Call method as MCP
|
271
291
|
################################################################################
|
272
292
|
try:
|
273
|
-
mcp_response = self.default_adapter(
|
293
|
+
mcp_response = self.default_adapter(raw_request, args)
|
274
294
|
except Exception as e:
|
275
295
|
error = json.dumps(SubroutineError.from_exception(e))
|
276
296
|
return self.app.error(error, mimetype=MIME.json.value)
|
@@ -369,8 +389,12 @@ class SSEService:
|
|
369
389
|
self.sse_dict = {}
|
370
390
|
|
371
391
|
# noinspection PyUnusedLocal
|
372
|
-
@api.get("/sse", tag=
|
373
|
-
def sse(
|
392
|
+
@api.get("/sse", tag=api.TAG_ENDPOINT)
|
393
|
+
def sse(
|
394
|
+
self,
|
395
|
+
raw_request: Any,
|
396
|
+
args: Optional[Dict[str, str]] = None
|
397
|
+
) -> Iterable[SSE]:
|
374
398
|
session_id = str(uuid.uuid4())
|
375
399
|
queue = Queue(8)
|
376
400
|
with self.lock:
|
@@ -395,8 +419,24 @@ class SSEService:
|
|
395
419
|
|
396
420
|
return _stream()
|
397
421
|
|
398
|
-
@api.route("/sse/message", tag=
|
399
|
-
def sse_message(
|
422
|
+
@api.route("/sse/message", tag=api.TAG_ENDPOINT)
|
423
|
+
def sse_message(
|
424
|
+
self,
|
425
|
+
raw_request: Any,
|
426
|
+
args: Optional[Dict[str, str]] = None
|
427
|
+
) -> None:
|
428
|
+
if not isinstance(raw_request, Dict):
|
429
|
+
raise ValueError(
|
430
|
+
f"/message only accepts JSON object as input. "
|
431
|
+
f"Expect \"dict\", got \"{type(raw_request)}\"."
|
432
|
+
)
|
433
|
+
|
434
|
+
if args is not None:
|
435
|
+
conflicts = args.keys() & raw_request.keys()
|
436
|
+
if len(conflicts) > 0:
|
437
|
+
raise ValueError(f"Duplicated fields: \"{conflicts}\".")
|
438
|
+
raw_request = {**args, **raw_request}
|
439
|
+
|
400
440
|
################################################################################
|
401
441
|
# session validation
|
402
442
|
################################################################################
|
@@ -471,8 +511,27 @@ class JSONRPCService:
|
|
471
511
|
|
472
512
|
self.type_adapter = TypeAdapter(Union[JSONRPCRequest, JSONRPCNotification])
|
473
513
|
|
474
|
-
@api.route(tag=
|
475
|
-
def message(
|
514
|
+
@api.route(tag=api.TAG_ENDPOINT)
|
515
|
+
def message(
|
516
|
+
self,
|
517
|
+
raw_request: Any,
|
518
|
+
args: Optional[Dict[str, str]] = None
|
519
|
+
) -> Union[JSONRPCResponse, Iterable[JSONRPCResponse], None]:
|
520
|
+
if isinstance(raw_request, List):
|
521
|
+
raise ValueError("Batching RCP requests is not supported yet.")
|
522
|
+
|
523
|
+
if not isinstance(raw_request, Dict):
|
524
|
+
raise ValueError(
|
525
|
+
f"/message only accepts JSON object as input. "
|
526
|
+
f"Expect \"dict\", got \"{type(raw_request)}\"."
|
527
|
+
)
|
528
|
+
|
529
|
+
if args is not None:
|
530
|
+
conflicts = args.keys() & raw_request.keys()
|
531
|
+
if len(conflicts) > 0:
|
532
|
+
raise ValueError(f"Duplicated fields: \"{conflicts}\".")
|
533
|
+
raw_request = {**args, **raw_request}
|
534
|
+
|
476
535
|
request = self.type_adapter.validate_python(raw_request)
|
477
536
|
path = f"/{request.method}"
|
478
537
|
route = self.service_routes.get(path, self.builtin_routes.get(path))
|
@@ -502,6 +561,10 @@ class LifeCycleService:
|
|
502
561
|
serverInfo=Implementation(name="python-libentry", version="1.0.0")
|
503
562
|
)
|
504
563
|
|
564
|
+
@api.route()
|
565
|
+
def ping(self):
|
566
|
+
return None
|
567
|
+
|
505
568
|
|
506
569
|
class NotificationsService:
|
507
570
|
|
@@ -521,7 +584,7 @@ class ToolsService:
|
|
521
584
|
self._tool_routes = {}
|
522
585
|
for route in self.service_routes.values():
|
523
586
|
api_info = route.api_info
|
524
|
-
if api_info.tag !=
|
587
|
+
if api_info.tag != api.TAG_TOOL:
|
525
588
|
continue
|
526
589
|
self._tool_routes[api_info.name] = route
|
527
590
|
return self._tool_routes
|
@@ -636,7 +699,7 @@ class ResourcesService:
|
|
636
699
|
self._resource_routes = {}
|
637
700
|
for route in self.service_routes.values():
|
638
701
|
api_info = route.api_info
|
639
|
-
if api_info.tag !=
|
702
|
+
if api_info.tag != api.TAG_RESOURCE:
|
640
703
|
continue
|
641
704
|
uri = api_info.model_extra.get("uri", api_info.path)
|
642
705
|
self._resource_routes[uri] = route
|
@@ -771,10 +834,15 @@ class FlaskServer(Flask):
|
|
771
834
|
logger.error(f"Async function \"{fn.__name__}\" is not supported.")
|
772
835
|
continue
|
773
836
|
|
774
|
-
logger.info(f"Serving {path} as {', '.join(methods)}.")
|
775
|
-
|
776
837
|
handler = FlaskHandler(fn, api_info, self)
|
777
838
|
routes[path] = Route(api_info=api_info, fn=fn, handler=handler)
|
839
|
+
|
840
|
+
mode = api.TAG_ENDPOINT
|
841
|
+
if isinstance(handler.default_adapter, SubroutineAdapter):
|
842
|
+
mode = api.TAG_SUBROUTINE
|
843
|
+
elif isinstance(handler.default_adapter, JSONRPCAdapter):
|
844
|
+
mode = api.TAG_JSONRPC
|
845
|
+
logger.info(f"{mode.capitalize()}:\tmethod={'|'.join(methods)}\tpath={path}")
|
778
846
|
return routes
|
779
847
|
|
780
848
|
def ok(self, body: Union[str, Iterable[str], None], mimetype: str):
|
libentry/resource.py
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
__author__ = "xi"
|
4
|
+
__all__ = [
|
5
|
+
"AbstractResourceManager",
|
6
|
+
"JSONResourceManager",
|
7
|
+
"RemoteResourceManager",
|
8
|
+
"ResourceManager",
|
9
|
+
]
|
10
|
+
|
11
|
+
import abc
|
12
|
+
from typing import Any, Dict, List
|
13
|
+
from urllib.parse import urlparse
|
14
|
+
|
15
|
+
import yaml
|
16
|
+
|
17
|
+
|
18
|
+
class AbstractResourceManager(abc.ABC):
|
19
|
+
|
20
|
+
@abc.abstractmethod
|
21
|
+
def list(self) -> List[str]:
|
22
|
+
pass
|
23
|
+
|
24
|
+
@abc.abstractmethod
|
25
|
+
def get(self, name: str) -> Any:
|
26
|
+
pass
|
27
|
+
|
28
|
+
@abc.abstractmethod
|
29
|
+
def set(self, name: str, value: Any) -> Any:
|
30
|
+
pass
|
31
|
+
|
32
|
+
|
33
|
+
class JSONResourceManager(AbstractResourceManager):
|
34
|
+
|
35
|
+
def __init__(self, path: str):
|
36
|
+
with open(path, "r") as f:
|
37
|
+
self.resource_obj = yaml.load(f, yaml.SafeLoader)
|
38
|
+
|
39
|
+
if not isinstance(self.resource_obj, Dict):
|
40
|
+
raise RuntimeError(
|
41
|
+
f"Expect a JSON object, "
|
42
|
+
f"got {type(self.resource_obj)}."
|
43
|
+
)
|
44
|
+
|
45
|
+
def list(self) -> List[str]:
|
46
|
+
return [*self.resource_obj.keys()]
|
47
|
+
|
48
|
+
def get(self, name: str) -> Any:
|
49
|
+
return self.resource_obj.get(name)
|
50
|
+
|
51
|
+
def set(self, name: str, value: Any) -> Any:
|
52
|
+
raise NotImplementedError("\"set\" is not supported by JSONResourceClient.")
|
53
|
+
|
54
|
+
|
55
|
+
class RemoteResourceManager(AbstractResourceManager):
|
56
|
+
|
57
|
+
def __init__(self, url: str):
|
58
|
+
pass
|
59
|
+
|
60
|
+
|
61
|
+
class ResourceManager:
|
62
|
+
|
63
|
+
def __new__(cls, uri: str) -> AbstractResourceManager:
|
64
|
+
parsed_uri = urlparse(uri)
|
65
|
+
if parsed_uri.scheme in {"", "json", "yaml", "yml"}:
|
66
|
+
return JSONResourceManager(parsed_uri.path)
|
67
|
+
else:
|
68
|
+
raise NotImplementedError(f"Scheme \"{parsed_uri.scheme}\" is not supported.")
|
69
|
+
|
70
|
+
|
71
|
+
def _test():
|
72
|
+
client = ResourceManager("config_test.yml")
|
73
|
+
print(client.get("qwen_2.5"))
|
74
|
+
return 0
|
75
|
+
|
76
|
+
|
77
|
+
if __name__ == "__main__":
|
78
|
+
raise SystemExit(_test())
|
@@ -6,13 +6,14 @@ libentry/executor.py,sha256=cTV0WxJi0nU1TP-cOwmeodN8DD6L1691M2HIQsJtGrU,6582
|
|
6
6
|
libentry/experiment.py,sha256=ejgAHDXWIe9x4haUzIFuz1WasLY0_aD1z_vyEVGjTu8,4922
|
7
7
|
libentry/json.py,sha256=CubUUu29h7idLaC4d66vKhjBgVHKN1rZOv-Tw2qM17k,1916
|
8
8
|
libentry/logging.py,sha256=IiYoCUzm8XTK1fduA-NA0FI2Qz_m81NEPV3d3tEfgdI,1349
|
9
|
+
libentry/resource.py,sha256=qAPXF7RzRYeCM4xWft6XR3SYk7lEiQ6ymOHbpNoi0mQ,1821
|
9
10
|
libentry/schema.py,sha256=40SOhCF_eytWOF47MWKCRHKHl_lCaQVetx1Af62PkiI,10439
|
10
11
|
libentry/test_api.py,sha256=Xw7B7sH6g1iCTV5sFzyBF3JAJzeOr9xg0AyezTNsnIk,4452
|
11
12
|
libentry/utils.py,sha256=O7P6GadtUIjq0N2IZH7PhHZDUM3NebzcqyDqytet7CM,683
|
12
13
|
libentry/mcp/__init__.py,sha256=1oLL20yLB1GL9IbFiZD8OReDqiCpFr-yetIR6x1cNkI,23
|
13
|
-
libentry/mcp/api.py,sha256=
|
14
|
+
libentry/mcp/api.py,sha256=KSvz6_TNGKQQoFJPNH1XIc8CMji0I2_CqC9VCSOPRfc,2491
|
14
15
|
libentry/mcp/client.py,sha256=tEJbMpXiMKQ0qDgLGyavE2A4jpoXliqbrVj_TMfOGXI,22488
|
15
|
-
libentry/mcp/service.py,sha256=
|
16
|
+
libentry/mcp/service.py,sha256=VBacwSN9xiV3wk5Ta0LHxRAor1cwR04ZavlKLTdTuWw,34721
|
16
17
|
libentry/mcp/types.py,sha256=xTQCnKAgeJNss4klJ33MrWHGCzG_LeR3urizO_Z9q9U,12239
|
17
18
|
libentry/service/__init__.py,sha256=1oLL20yLB1GL9IbFiZD8OReDqiCpFr-yetIR6x1cNkI,23
|
18
19
|
libentry/service/common.py,sha256=OVaW2afgKA6YqstJmtnprBCqQEUZEWotZ6tHavmJJeU,42
|
@@ -21,10 +22,10 @@ libentry/service/list.py,sha256=ElHWhTgShGOhaxMUEwVbMXos0NQKjHsODboiQ-3AMwE,1397
|
|
21
22
|
libentry/service/running.py,sha256=FrPJoJX6wYxcHIysoatAxhW3LajCCm0Gx6l7__6sULQ,5105
|
22
23
|
libentry/service/start.py,sha256=mZT7b9rVULvzy9GTZwxWnciCHgv9dbGN2JbxM60OMn4,1270
|
23
24
|
libentry/service/stop.py,sha256=wOpwZgrEJ7QirntfvibGq-XsTC6b3ELhzRW2zezh-0s,1187
|
24
|
-
libentry-1.
|
25
|
-
libentry-1.
|
26
|
-
libentry-1.
|
27
|
-
libentry-1.
|
28
|
-
libentry-1.
|
29
|
-
libentry-1.
|
30
|
-
libentry-1.
|
25
|
+
libentry-1.24.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
26
|
+
libentry-1.24.dist-info/METADATA,sha256=zvGz_iBvBEMl_1HH4odL37ySnpBaLTTo7dAsxdwNrB4,1133
|
27
|
+
libentry-1.24.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
28
|
+
libentry-1.24.dist-info/entry_points.txt,sha256=1v_nLVDsjvVJp9SWhl4ef2zZrsLTBtFWgrYFgqvQBgc,61
|
29
|
+
libentry-1.24.dist-info/top_level.txt,sha256=u2uF6-X5fn2Erf9PYXOg_6tntPqTpyT-yzUZrltEd6I,9
|
30
|
+
libentry-1.24.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
31
|
+
libentry-1.24.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|