lmnr 0.2.9__tar.gz → 0.2.10__tar.gz
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-0.2.9 → lmnr-0.2.10}/PKG-INFO +1 -1
- {lmnr-0.2.9 → lmnr-0.2.10}/pyproject.toml +2 -2
- lmnr-0.2.10/src/lmnr/__init__.py +4 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/cli.py +42 -0
- lmnr-0.2.10/src/lmnr/cli/zip.py +12 -0
- lmnr-0.2.10/src/lmnr/sdk/__init__.py +0 -0
- lmnr-0.2.10/src/lmnr/sdk/registry.py +29 -0
- lmnr-0.2.10/src/lmnr/sdk/remote_debugger.py +136 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/types.py +10 -1
- lmnr-0.2.9/src/lmnr/__init__.py +0 -8
- lmnr-0.2.9/src/lmnr/sdk/remote_debugger.py +0 -140
- {lmnr-0.2.9 → lmnr-0.2.10}/LICENSE +0 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/README.md +0 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/__init__.py +0 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/__main__.py +0 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/__init__.py +0 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/__init__.py +0 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/code.py +0 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/condition.py +0 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/input.py +0 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/json_extractor.py +0 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/llm.py +0 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/output.py +0 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/router.py +0 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/semantic_search.py +0 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/types.py +0 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/parser.py +0 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/utils.py +0 -0
- {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/sdk/endpoint.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "lmnr"
|
3
|
-
version = "0.2.
|
3
|
+
version = "0.2.10"
|
4
4
|
description = "Python SDK for Laminar AI"
|
5
5
|
authors = [
|
6
6
|
{ name = "lmnr.ai", email = "founders@lmnr.ai" }
|
@@ -11,7 +11,7 @@ license = "Apache-2.0"
|
|
11
11
|
|
12
12
|
[tool.poetry]
|
13
13
|
name = "lmnr"
|
14
|
-
version = "0.2.
|
14
|
+
version = "0.2.10"
|
15
15
|
description = "Python SDK for Laminar AI"
|
16
16
|
authors = ["lmnr.ai"]
|
17
17
|
readme = "README.md"
|
@@ -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)
|
@@ -0,0 +1,12 @@
|
|
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
|
+
file_path = os.path.join(root, file)
|
11
|
+
arcname = os.path.relpath(file_path, directory_path)
|
12
|
+
zipf.write(file_path, arcname)
|
File without changes
|
@@ -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
|
@@ -0,0 +1,136 @@
|
|
1
|
+
from typing import Callable, Optional, Union
|
2
|
+
from websockets.sync.client import connect
|
3
|
+
import pydantic
|
4
|
+
import websockets
|
5
|
+
from lmnr.types import (
|
6
|
+
DeregisterDebuggerRequest,
|
7
|
+
NodeFunction,
|
8
|
+
NodeInput,
|
9
|
+
RegisterDebuggerRequest,
|
10
|
+
SDKError,
|
11
|
+
ToolCallError,
|
12
|
+
ToolCallRequest,
|
13
|
+
ToolCallResponse,
|
14
|
+
)
|
15
|
+
import uuid
|
16
|
+
import json
|
17
|
+
from threading import Thread
|
18
|
+
|
19
|
+
|
20
|
+
class RemoteDebugger:
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
project_api_key: str,
|
24
|
+
tools: Union[dict[str, NodeFunction], list[Callable[..., NodeInput]]] = [],
|
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
|
+
|
30
|
+
self.project_api_key = project_api_key
|
31
|
+
self.url = "wss://api.lmnr.ai/v2/endpoint/ws"
|
32
|
+
self.tools = tools
|
33
|
+
self.thread = Thread(target=self._run)
|
34
|
+
self.stop_flag = False
|
35
|
+
self.session = None
|
36
|
+
|
37
|
+
def start(self) -> Optional[str]:
|
38
|
+
self.stop_flag = False
|
39
|
+
self.session = self._generate_session_id()
|
40
|
+
self.thread.start()
|
41
|
+
return self.session
|
42
|
+
|
43
|
+
def stop(self):
|
44
|
+
self.stop_flag = True
|
45
|
+
self.thread.join()
|
46
|
+
self.session = None
|
47
|
+
# python allows running threads only once, so we need to create
|
48
|
+
# a new thread
|
49
|
+
# in case the user wants to start the debugger again
|
50
|
+
self.thread = Thread(target=self._run)
|
51
|
+
|
52
|
+
def _run(self):
|
53
|
+
assert self.session is not None, "Session ID not set"
|
54
|
+
request = RegisterDebuggerRequest(debuggerSessionId=self.session)
|
55
|
+
with connect(
|
56
|
+
self.url,
|
57
|
+
additional_headers={"Authorization": f"Bearer {self.project_api_key}"},
|
58
|
+
) as websocket:
|
59
|
+
websocket.send(request.model_dump_json())
|
60
|
+
print(self._format_session_id_and_registerd_functions())
|
61
|
+
req_id = None
|
62
|
+
|
63
|
+
while not self.stop_flag:
|
64
|
+
try:
|
65
|
+
# blocks the thread until a message
|
66
|
+
# is received or a timeout (3 seconds) occurs
|
67
|
+
message = websocket.recv(3)
|
68
|
+
except TimeoutError:
|
69
|
+
continue
|
70
|
+
except websockets.exceptions.ConnectionClosedError:
|
71
|
+
print("Connection closed. Please restart the debugger.")
|
72
|
+
return
|
73
|
+
try:
|
74
|
+
tool_call = ToolCallRequest.model_validate_json(message)
|
75
|
+
req_id = tool_call.reqId
|
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
|
+
)
|
85
|
+
e = ToolCallError(error=error_message, reqId=req_id)
|
86
|
+
websocket.send(e.model_dump_json())
|
87
|
+
continue
|
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
|
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())
|
115
|
+
websocket.send(
|
116
|
+
DeregisterDebuggerRequest(
|
117
|
+
debuggerSessionId=self.session, deregister=True
|
118
|
+
).model_dump_json()
|
119
|
+
)
|
120
|
+
|
121
|
+
def _generate_session_id(self) -> str:
|
122
|
+
return uuid.uuid4().urn[9:]
|
123
|
+
|
124
|
+
def _format_session_id_and_registerd_functions(self) -> str:
|
125
|
+
registered_functions = ",\n".join(["- " + k for k in self.tools.keys()])
|
126
|
+
return f"""
|
127
|
+
========================================
|
128
|
+
Debugger Session ID:
|
129
|
+
{self.session}
|
130
|
+
========================================
|
131
|
+
|
132
|
+
Registered functions:
|
133
|
+
{registered_functions}
|
134
|
+
|
135
|
+
========================================
|
136
|
+
"""
|
@@ -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
|
lmnr-0.2.9/src/lmnr/__init__.py
DELETED
@@ -1,140 +0,0 @@
|
|
1
|
-
from typing import Callable, Optional
|
2
|
-
from websockets.sync.client import connect
|
3
|
-
import pydantic
|
4
|
-
import websockets
|
5
|
-
from lmnr.types import (
|
6
|
-
DeregisterDebuggerRequest, NodeInput, RegisterDebuggerRequest,
|
7
|
-
SDKError, ToolCallError, ToolCallRequest, ToolCallResponse
|
8
|
-
)
|
9
|
-
import uuid
|
10
|
-
import json
|
11
|
-
from threading import Thread
|
12
|
-
|
13
|
-
class RemoteDebugger:
|
14
|
-
def __init__(
|
15
|
-
self,
|
16
|
-
project_api_key: str,
|
17
|
-
tools: list[Callable[..., NodeInput]] = []
|
18
|
-
):
|
19
|
-
self.project_api_key = project_api_key
|
20
|
-
self.url = 'wss://api.lmnr.ai/v2/endpoint/ws'
|
21
|
-
self.tools = tools
|
22
|
-
self.thread = Thread(target=self._run)
|
23
|
-
self.stop_flag = False
|
24
|
-
self.session = None
|
25
|
-
|
26
|
-
def start(self) -> Optional[str]:
|
27
|
-
self.stop_flag = False
|
28
|
-
self.session = self._generate_session_id()
|
29
|
-
self.thread.start()
|
30
|
-
return self.session
|
31
|
-
|
32
|
-
def stop(self):
|
33
|
-
self.stop_flag = True
|
34
|
-
self.thread.join()
|
35
|
-
self.session = None
|
36
|
-
# python allows running threads only once, so we need to create
|
37
|
-
# a new thread
|
38
|
-
# in case the user wants to start the debugger again
|
39
|
-
self.thread = Thread(target=self._run)
|
40
|
-
|
41
|
-
def get_session_id(self) -> str:
|
42
|
-
return self.session
|
43
|
-
|
44
|
-
def _run(self):
|
45
|
-
request = RegisterDebuggerRequest(debuggerSessionId=self.session)
|
46
|
-
with connect(
|
47
|
-
self.url,
|
48
|
-
additional_headers={
|
49
|
-
'Authorization': f'Bearer {self.project_api_key}'
|
50
|
-
}
|
51
|
-
) as websocket:
|
52
|
-
websocket.send(request.model_dump_json())
|
53
|
-
print(self._format_session_id_and_registerd_functions())
|
54
|
-
req_id = None
|
55
|
-
|
56
|
-
while not self.stop_flag:
|
57
|
-
try:
|
58
|
-
# blocks the thread until a message
|
59
|
-
# is received or a timeout (3 seconds) occurs
|
60
|
-
message = websocket.recv(3)
|
61
|
-
except TimeoutError:
|
62
|
-
continue
|
63
|
-
except websockets.exceptions.ConnectionClosedError:
|
64
|
-
print("Connection closed. Please restart the debugger.")
|
65
|
-
return
|
66
|
-
try:
|
67
|
-
tool_call = ToolCallRequest.model_validate_json(message)
|
68
|
-
req_id = tool_call.reqId
|
69
|
-
except:
|
70
|
-
raise SDKError(f'Invalid message received:\n{message}')
|
71
|
-
matching_tools = [
|
72
|
-
tool for tool in self.tools
|
73
|
-
if tool.__name__ == tool_call.toolCall.function.name
|
74
|
-
]
|
75
|
-
if not matching_tools:
|
76
|
-
error_message = \
|
77
|
-
f'Tool {tool_call.toolCall.function.name} not found' +\
|
78
|
-
'. Registered tools: ' +\
|
79
|
-
{", ".join([tool.__name__ for tool in self.tools])}
|
80
|
-
e = ToolCallError(error=error_message, reqId=req_id)
|
81
|
-
websocket.send(e.model_dump_json())
|
82
|
-
continue
|
83
|
-
tool = matching_tools[0]
|
84
|
-
if tool.__name__ == tool_call.toolCall.function.name:
|
85
|
-
# default the arguments to an empty dictionary
|
86
|
-
arguments = {}
|
87
|
-
try:
|
88
|
-
arguments = json.loads(
|
89
|
-
tool_call.toolCall.function.arguments)
|
90
|
-
except:
|
91
|
-
pass
|
92
|
-
try:
|
93
|
-
response = tool(**arguments)
|
94
|
-
except Exception as e:
|
95
|
-
error_message = 'Error occurred while running tool' +\
|
96
|
-
f'{tool.__name__}: {e}'
|
97
|
-
e = ToolCallError(error=error_message, reqId=req_id)
|
98
|
-
websocket.send(e.model_dump_json())
|
99
|
-
continue
|
100
|
-
formatted_response = None
|
101
|
-
try:
|
102
|
-
formatted_response = ToolCallResponse(
|
103
|
-
reqId=tool_call.reqId,
|
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()
|
113
|
-
)
|
114
|
-
websocket.send(
|
115
|
-
DeregisterDebuggerRequest(
|
116
|
-
debuggerSessionId=self.session,
|
117
|
-
deregister=True
|
118
|
-
).model_dump_json()
|
119
|
-
)
|
120
|
-
|
121
|
-
def _generate_session_id(self) -> str:
|
122
|
-
return uuid.uuid4().urn[9:]
|
123
|
-
|
124
|
-
def _format_session_id_and_registerd_functions(self) -> str:
|
125
|
-
registered_functions = \
|
126
|
-
',\n'.join(['- ' + tool.__name__ for tool in self.tools])
|
127
|
-
return \
|
128
|
-
f"""
|
129
|
-
========================================
|
130
|
-
Debugger Session ID:
|
131
|
-
{self.session}
|
132
|
-
========================================
|
133
|
-
|
134
|
-
Registered functions:
|
135
|
-
{registered_functions}
|
136
|
-
|
137
|
-
========================================
|
138
|
-
"""
|
139
|
-
|
140
|
-
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|