lmnr 0.2.9__py3-none-any.whl → 0.2.11__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.
- lmnr/__init__.py +3 -7
- lmnr/cli/cli.py +42 -0
- lmnr/cli/zip.py +16 -0
- lmnr/sdk/__init__.py +0 -0
- lmnr/sdk/registry.py +29 -0
- lmnr/sdk/remote_debugger.py +60 -64
- lmnr/types.py +10 -1
- {lmnr-0.2.9.dist-info → lmnr-0.2.11.dist-info}/METADATA +1 -1
- {lmnr-0.2.9.dist-info → lmnr-0.2.11.dist-info}/RECORD +12 -9
- {lmnr-0.2.9.dist-info → lmnr-0.2.11.dist-info}/LICENSE +0 -0
- {lmnr-0.2.9.dist-info → lmnr-0.2.11.dist-info}/WHEEL +0 -0
- {lmnr-0.2.9.dist-info → lmnr-0.2.11.dist-info}/entry_points.txt +0 -0
lmnr/__init__.py
CHANGED
@@ -1,8 +1,4 @@
|
|
1
1
|
from .sdk.endpoint import Laminar
|
2
|
-
from .types import
|
3
|
-
|
4
|
-
|
5
|
-
EndpointRunResponse,
|
6
|
-
NodeInput
|
7
|
-
)
|
8
|
-
from .sdk.remote_debugger import RemoteDebugger as LaminarRemoteDebugger
|
2
|
+
from .types import ChatMessage, EndpointRunError, EndpointRunResponse, NodeInput
|
3
|
+
from .sdk.remote_debugger import RemoteDebugger as LaminarRemoteDebugger
|
4
|
+
from .sdk.registry import Registry as Pipeline
|
lmnr/cli/cli.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
from pathlib import Path
|
1
2
|
import requests
|
2
3
|
from dotenv import load_dotenv
|
3
4
|
import os
|
@@ -6,6 +7,8 @@ import logging
|
|
6
7
|
from cookiecutter.main import cookiecutter
|
7
8
|
from pydantic.alias_generators import to_pascal
|
8
9
|
|
10
|
+
from lmnr.cli.zip import zip_directory
|
11
|
+
|
9
12
|
from .parser.parser import runnable_graph_to_template_vars
|
10
13
|
|
11
14
|
logger = logging.getLogger(__name__)
|
@@ -92,3 +95,42 @@ def pull(pipeline_name, pipeline_version_name, project_api_key, loglevel):
|
|
92
95
|
no_input=True,
|
93
96
|
overwrite_if_exists=True,
|
94
97
|
)
|
98
|
+
|
99
|
+
|
100
|
+
@cli.command(name="deploy")
|
101
|
+
@click.argument("endpoint_id")
|
102
|
+
@click.option(
|
103
|
+
"-p",
|
104
|
+
"--project-api-key",
|
105
|
+
help="Project API key",
|
106
|
+
)
|
107
|
+
def deploy(endpoint_id, project_api_key):
|
108
|
+
project_api_key = project_api_key or os.environ.get("LMNR_PROJECT_API_KEY")
|
109
|
+
if not project_api_key:
|
110
|
+
load_dotenv()
|
111
|
+
project_api_key = os.environ.get("LMNR_PROJECT_API_KEY")
|
112
|
+
if not project_api_key:
|
113
|
+
raise ValueError("LMNR_PROJECT_API_KEY is not set")
|
114
|
+
|
115
|
+
current_directory = Path.cwd()
|
116
|
+
zip_file_path = current_directory / "archive.zip"
|
117
|
+
|
118
|
+
zip_directory(current_directory, zip_file_path)
|
119
|
+
|
120
|
+
try:
|
121
|
+
url = f"https://api.lmnr.ai/v2/endpoints/{endpoint_id}/deploy-code"
|
122
|
+
with open(zip_file_path, "rb") as f:
|
123
|
+
headers = {
|
124
|
+
"Authorization": f"Bearer {project_api_key}",
|
125
|
+
}
|
126
|
+
files = {"file": f}
|
127
|
+
response = requests.post(url, headers=headers, files=files)
|
128
|
+
|
129
|
+
if response.status_code != 200:
|
130
|
+
raise ValueError(
|
131
|
+
f"Error in deploying code: {response.status_code}\n{response.text}"
|
132
|
+
)
|
133
|
+
except Exception:
|
134
|
+
logging.exception("Error in deploying code")
|
135
|
+
finally:
|
136
|
+
Path.unlink(zip_file_path, missing_ok=True)
|
lmnr/cli/zip.py
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
import os
|
2
|
+
from pathlib import Path
|
3
|
+
import zipfile
|
4
|
+
|
5
|
+
|
6
|
+
def zip_directory(directory_path: Path, zip_file_path: Path):
|
7
|
+
with zipfile.ZipFile(zip_file_path, "w", zipfile.ZIP_DEFLATED) as zipf:
|
8
|
+
for root, _, files in os.walk(directory_path):
|
9
|
+
for file in files:
|
10
|
+
# Don't include the zip file itself, otherwise goes to infinite loop
|
11
|
+
if file == zip_file_path.name:
|
12
|
+
continue
|
13
|
+
|
14
|
+
file_path = os.path.join(root, file)
|
15
|
+
arcname = os.path.relpath(file_path, directory_path)
|
16
|
+
zipf.write(file_path, arcname)
|
lmnr/sdk/__init__.py
ADDED
File without changes
|
lmnr/sdk/registry.py
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
from typing import Callable
|
2
|
+
|
3
|
+
from lmnr.types import NodeFunction, NodeInput
|
4
|
+
|
5
|
+
|
6
|
+
class Registry:
|
7
|
+
"""
|
8
|
+
Class to register and resolve node functions based on their node names.
|
9
|
+
|
10
|
+
Node names cannot have space in their name.
|
11
|
+
"""
|
12
|
+
|
13
|
+
functions: dict[str, NodeFunction]
|
14
|
+
|
15
|
+
def __init__(self):
|
16
|
+
self.functions = {}
|
17
|
+
|
18
|
+
def add(self, node_name: str, function: Callable[..., NodeInput]):
|
19
|
+
self.functions[node_name] = NodeFunction(node_name, function)
|
20
|
+
|
21
|
+
def func(self, node_name: str):
|
22
|
+
def decorator(f: Callable[..., NodeInput]):
|
23
|
+
self.add(node_name, f)
|
24
|
+
return f
|
25
|
+
|
26
|
+
return decorator
|
27
|
+
|
28
|
+
def get(self, node_name: str) -> Callable[..., NodeInput]:
|
29
|
+
return self.functions[node_name].function
|
lmnr/sdk/remote_debugger.py
CHANGED
@@ -1,34 +1,45 @@
|
|
1
|
-
from typing import Callable, Optional
|
1
|
+
from typing import Callable, Optional, Union
|
2
2
|
from websockets.sync.client import connect
|
3
3
|
import pydantic
|
4
4
|
import websockets
|
5
5
|
from lmnr.types import (
|
6
|
-
DeregisterDebuggerRequest,
|
7
|
-
|
6
|
+
DeregisterDebuggerRequest,
|
7
|
+
NodeFunction,
|
8
|
+
NodeInput,
|
9
|
+
RegisterDebuggerRequest,
|
10
|
+
SDKError,
|
11
|
+
ToolCallError,
|
12
|
+
ToolCallRequest,
|
13
|
+
ToolCallResponse,
|
8
14
|
)
|
9
15
|
import uuid
|
10
16
|
import json
|
11
17
|
from threading import Thread
|
12
18
|
|
19
|
+
|
13
20
|
class RemoteDebugger:
|
14
21
|
def __init__(
|
15
22
|
self,
|
16
23
|
project_api_key: str,
|
17
|
-
tools: list[Callable[..., NodeInput]] = []
|
24
|
+
tools: Union[dict[str, NodeFunction], list[Callable[..., NodeInput]]] = [],
|
18
25
|
):
|
26
|
+
# for simplicity and backwards compatibility, we allow the user to pass a list
|
27
|
+
if isinstance(tools, list):
|
28
|
+
tools = {f.__name__: NodeFunction(f.__name__, f) for f in tools}
|
29
|
+
|
19
30
|
self.project_api_key = project_api_key
|
20
|
-
self.url =
|
31
|
+
self.url = "wss://api.lmnr.ai/v2/endpoint/ws"
|
21
32
|
self.tools = tools
|
22
33
|
self.thread = Thread(target=self._run)
|
23
34
|
self.stop_flag = False
|
24
35
|
self.session = None
|
25
|
-
|
36
|
+
|
26
37
|
def start(self) -> Optional[str]:
|
27
38
|
self.stop_flag = False
|
28
39
|
self.session = self._generate_session_id()
|
29
40
|
self.thread.start()
|
30
41
|
return self.session
|
31
|
-
|
42
|
+
|
32
43
|
def stop(self):
|
33
44
|
self.stop_flag = True
|
34
45
|
self.thread.join()
|
@@ -37,17 +48,13 @@ class RemoteDebugger:
|
|
37
48
|
# a new thread
|
38
49
|
# in case the user wants to start the debugger again
|
39
50
|
self.thread = Thread(target=self._run)
|
40
|
-
|
41
|
-
def get_session_id(self) -> str:
|
42
|
-
return self.session
|
43
|
-
|
51
|
+
|
44
52
|
def _run(self):
|
53
|
+
assert self.session is not None, "Session ID not set"
|
45
54
|
request = RegisterDebuggerRequest(debuggerSessionId=self.session)
|
46
55
|
with connect(
|
47
56
|
self.url,
|
48
|
-
additional_headers={
|
49
|
-
'Authorization': f'Bearer {self.project_api_key}'
|
50
|
-
}
|
57
|
+
additional_headers={"Authorization": f"Bearer {self.project_api_key}"},
|
51
58
|
) as websocket:
|
52
59
|
websocket.send(request.model_dump_json())
|
53
60
|
print(self._format_session_id_and_registerd_functions())
|
@@ -55,7 +62,7 @@ class RemoteDebugger:
|
|
55
62
|
|
56
63
|
while not self.stop_flag:
|
57
64
|
try:
|
58
|
-
# blocks the thread until a message
|
65
|
+
# blocks the thread until a message
|
59
66
|
# is received or a timeout (3 seconds) occurs
|
60
67
|
message = websocket.recv(3)
|
61
68
|
except TimeoutError:
|
@@ -66,55 +73,48 @@ class RemoteDebugger:
|
|
66
73
|
try:
|
67
74
|
tool_call = ToolCallRequest.model_validate_json(message)
|
68
75
|
req_id = tool_call.reqId
|
69
|
-
except:
|
70
|
-
raise SDKError(f
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
'. Registered tools: ' +\
|
79
|
-
{", ".join([tool.__name__ for tool in self.tools])}
|
76
|
+
except Exception:
|
77
|
+
raise SDKError(f"Invalid message received:\n{message}")
|
78
|
+
matching_tool = self.tools.get(tool_call.toolCall.function.name)
|
79
|
+
if matching_tool is None:
|
80
|
+
error_message = (
|
81
|
+
f"Tool {tool_call.toolCall.function.name} not found"
|
82
|
+
+ ". Registered tools: "
|
83
|
+
+ ", ".join(self.tools.keys())
|
84
|
+
)
|
80
85
|
e = ToolCallError(error=error_message, reqId=req_id)
|
81
86
|
websocket.send(e.model_dump_json())
|
82
87
|
continue
|
83
|
-
tool =
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
response=response
|
105
|
-
)
|
106
|
-
except pydantic.ValidationError as e:
|
107
|
-
formatted_response = ToolCallResponse(
|
108
|
-
reqId=tool_call.reqId,
|
109
|
-
response=str(response)
|
110
|
-
)
|
111
|
-
websocket.send(
|
112
|
-
formatted_response.model_dump_json()
|
88
|
+
tool = matching_tool.function
|
89
|
+
|
90
|
+
# default the arguments to an empty dictionary
|
91
|
+
arguments = {}
|
92
|
+
try:
|
93
|
+
arguments = json.loads(tool_call.toolCall.function.arguments)
|
94
|
+
except Exception:
|
95
|
+
pass
|
96
|
+
try:
|
97
|
+
response = tool(**arguments)
|
98
|
+
except Exception as e:
|
99
|
+
error_message = (
|
100
|
+
"Error occurred while running tool" + f"{tool.__name__}: {e}"
|
101
|
+
)
|
102
|
+
e = ToolCallError(error=error_message, reqId=req_id)
|
103
|
+
websocket.send(e.model_dump_json())
|
104
|
+
continue
|
105
|
+
formatted_response = None
|
106
|
+
try:
|
107
|
+
formatted_response = ToolCallResponse(
|
108
|
+
reqId=tool_call.reqId, response=response
|
113
109
|
)
|
110
|
+
except pydantic.ValidationError:
|
111
|
+
formatted_response = ToolCallResponse(
|
112
|
+
reqId=tool_call.reqId, response=str(response)
|
113
|
+
)
|
114
|
+
websocket.send(formatted_response.model_dump_json())
|
114
115
|
websocket.send(
|
115
116
|
DeregisterDebuggerRequest(
|
116
|
-
debuggerSessionId=self.session,
|
117
|
-
deregister=True
|
117
|
+
debuggerSessionId=self.session, deregister=True
|
118
118
|
).model_dump_json()
|
119
119
|
)
|
120
120
|
|
@@ -122,10 +122,8 @@ class RemoteDebugger:
|
|
122
122
|
return uuid.uuid4().urn[9:]
|
123
123
|
|
124
124
|
def _format_session_id_and_registerd_functions(self) -> str:
|
125
|
-
registered_functions =
|
126
|
-
|
127
|
-
return \
|
128
|
-
f"""
|
125
|
+
registered_functions = ",\n".join(["- " + k for k in self.tools.keys()])
|
126
|
+
return f"""
|
129
127
|
========================================
|
130
128
|
Debugger Session ID:
|
131
129
|
{self.session}
|
@@ -136,5 +134,3 @@ Registered functions:
|
|
136
134
|
|
137
135
|
========================================
|
138
136
|
"""
|
139
|
-
|
140
|
-
|
lmnr/types.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import requests
|
2
2
|
import pydantic
|
3
3
|
import uuid
|
4
|
-
from typing import Union, Optional
|
4
|
+
from typing import Callable, Union, Optional
|
5
5
|
|
6
6
|
|
7
7
|
class ChatMessage(pydantic.BaseModel):
|
@@ -90,3 +90,12 @@ class RegisterDebuggerRequest(pydantic.BaseModel):
|
|
90
90
|
class DeregisterDebuggerRequest(pydantic.BaseModel):
|
91
91
|
debuggerSessionId: str
|
92
92
|
deregister: bool
|
93
|
+
|
94
|
+
|
95
|
+
class NodeFunction:
|
96
|
+
node_name: str
|
97
|
+
function: Callable[..., NodeInput]
|
98
|
+
|
99
|
+
def __init__(self, node_name: str, function: Callable[..., NodeInput]):
|
100
|
+
self.node_name = node_name
|
101
|
+
self.function = function
|
@@ -1,7 +1,7 @@
|
|
1
|
-
lmnr/__init__.py,sha256=
|
1
|
+
lmnr/__init__.py,sha256=BBJ87AiHC_OpaYOCzF8QSsf7eO3LlJPOCBXHNKurbE4,235
|
2
2
|
lmnr/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
3
|
lmnr/cli/__main__.py,sha256=8hDtWlaFZK24KhfNq_ZKgtXqYHsDQDetukOCMlsbW0Q,59
|
4
|
-
lmnr/cli/cli.py,sha256=
|
4
|
+
lmnr/cli/cli.py,sha256=qG3-aY2EJIW3BSD-_s1vUsxHcxC9kiuIeAO8aVUq3ik,4103
|
5
5
|
lmnr/cli/parser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
6
|
lmnr/cli/parser/nodes/__init__.py,sha256=2MkPdKulb1kuNe6aT71CaqBA8iBrXyb5pq5bu_EvCb8,1052
|
7
7
|
lmnr/cli/parser/nodes/code.py,sha256=8lTPBibUzaw_t-9QoPljhxH3KA4CLn9DJjA-iWpprOA,933
|
@@ -15,11 +15,14 @@ lmnr/cli/parser/nodes/semantic_search.py,sha256=DWDPpV78XZ7vPIaPd86FbeDFAnKah4e6
|
|
15
15
|
lmnr/cli/parser/nodes/types.py,sha256=OVXj-iMEDY9nPKCX1-zddtoszZcUL3CXYYryI7O3et0,6094
|
16
16
|
lmnr/cli/parser/parser.py,sha256=yDa-ysAkh6si_hHU8Gw8EdtNWc4pFc5RbvgWEXGEPys,2370
|
17
17
|
lmnr/cli/parser/utils.py,sha256=1oy6BApHXOF7BTXbP8v3Oi9bwOdWZjoxDlRIOfXVxro,1169
|
18
|
+
lmnr/cli/zip.py,sha256=u2-LcYtQdZ_FIW0-PM-WGjclNPoB8v6OecrI79PyLPw,607
|
19
|
+
lmnr/sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
20
|
lmnr/sdk/endpoint.py,sha256=0HjcxMUcJz-klFZO2f5xtTaoLjcaEb8vrJ_YldTWUc8,7467
|
19
|
-
lmnr/sdk/
|
20
|
-
lmnr/
|
21
|
-
lmnr
|
22
|
-
lmnr-0.2.
|
23
|
-
lmnr-0.2.
|
24
|
-
lmnr-0.2.
|
25
|
-
lmnr-0.2.
|
21
|
+
lmnr/sdk/registry.py,sha256=sEYQFOjO72YvgBSEkBrvoewFExoyBzx6nELgBarvD6Y,755
|
22
|
+
lmnr/sdk/remote_debugger.py,sha256=BYAN13KUDxH412qD3HXdDH0RfokrePquDt35fzB7GUg,5010
|
23
|
+
lmnr/types.py,sha256=3HpLBQZr6F5YMISHYLnzzyrTwUttNqJxpyobw31YYJQ,2347
|
24
|
+
lmnr-0.2.11.dist-info/LICENSE,sha256=67b_wJHVV1CBaWkrKFWU1wyqTPSdzH77Ls-59631COg,10411
|
25
|
+
lmnr-0.2.11.dist-info/METADATA,sha256=Vy8sFZj2301oMMAdDWMnqa23iWBSzBuy68WhEgvlVZU,5565
|
26
|
+
lmnr-0.2.11.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
27
|
+
lmnr-0.2.11.dist-info/entry_points.txt,sha256=Qg7ZRax4k-rcQsZ26XRYQ8YFSBiyY2PNxYfq4a6PYXI,41
|
28
|
+
lmnr-0.2.11.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|