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.
Files changed (29) hide show
  1. {lmnr-0.2.9 → lmnr-0.2.10}/PKG-INFO +1 -1
  2. {lmnr-0.2.9 → lmnr-0.2.10}/pyproject.toml +2 -2
  3. lmnr-0.2.10/src/lmnr/__init__.py +4 -0
  4. {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/cli.py +42 -0
  5. lmnr-0.2.10/src/lmnr/cli/zip.py +12 -0
  6. lmnr-0.2.10/src/lmnr/sdk/__init__.py +0 -0
  7. lmnr-0.2.10/src/lmnr/sdk/registry.py +29 -0
  8. lmnr-0.2.10/src/lmnr/sdk/remote_debugger.py +136 -0
  9. {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/types.py +10 -1
  10. lmnr-0.2.9/src/lmnr/__init__.py +0 -8
  11. lmnr-0.2.9/src/lmnr/sdk/remote_debugger.py +0 -140
  12. {lmnr-0.2.9 → lmnr-0.2.10}/LICENSE +0 -0
  13. {lmnr-0.2.9 → lmnr-0.2.10}/README.md +0 -0
  14. {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/__init__.py +0 -0
  15. {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/__main__.py +0 -0
  16. {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/__init__.py +0 -0
  17. {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/__init__.py +0 -0
  18. {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/code.py +0 -0
  19. {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/condition.py +0 -0
  20. {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/input.py +0 -0
  21. {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/json_extractor.py +0 -0
  22. {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/llm.py +0 -0
  23. {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/output.py +0 -0
  24. {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/router.py +0 -0
  25. {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/semantic_search.py +0 -0
  26. {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/nodes/types.py +0 -0
  27. {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/parser.py +0 -0
  28. {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/cli/parser/utils.py +0 -0
  29. {lmnr-0.2.9 → lmnr-0.2.10}/src/lmnr/sdk/endpoint.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lmnr
3
- Version: 0.2.9
3
+ Version: 0.2.10
4
4
  Summary: Python SDK for Laminar AI
5
5
  License: Apache-2.0
6
6
  Author: lmnr.ai
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lmnr"
3
- version = "0.2.9"
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.9"
14
+ version = "0.2.10"
15
15
  description = "Python SDK for Laminar AI"
16
16
  authors = ["lmnr.ai"]
17
17
  readme = "README.md"
@@ -0,0 +1,4 @@
1
+ from .sdk.endpoint import Laminar
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
@@ -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
@@ -1,8 +0,0 @@
1
- from .sdk.endpoint import Laminar
2
- from .types import (
3
- ChatMessage,
4
- EndpointRunError,
5
- EndpointRunResponse,
6
- NodeInput
7
- )
8
- from .sdk.remote_debugger import RemoteDebugger as LaminarRemoteDebugger
@@ -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