@lmnr-ai/lmnr 0.2.6 → 0.3.0
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.
- package/README.md +3 -92
- package/package.json +8 -11
- package/src/index.ts +19 -103
- package/src/tracing.ts +0 -0
- package/src/types.ts +3 -46
- package/src/remote_debugger.ts +0 -111
package/README.md
CHANGED
|
@@ -14,9 +14,9 @@ npm install @lmnr-ai/lmnr
|
|
|
14
14
|
|
|
15
15
|
## Making Laminar endpoint calls
|
|
16
16
|
|
|
17
|
-
After you are ready to use your pipeline in your code, deploy it in Laminar
|
|
17
|
+
After you are ready to use your pipeline in your code, deploy it in Laminar by selecting the target version for the pipeline.
|
|
18
18
|
|
|
19
|
-
Once your pipeline is
|
|
19
|
+
Once your pipeline target is set, you can call it from JS in just a few lines.
|
|
20
20
|
|
|
21
21
|
Example use:
|
|
22
22
|
|
|
@@ -25,7 +25,7 @@ import { Laminar, NodeInput } from '@lmnr-ai/lmnr';
|
|
|
25
25
|
|
|
26
26
|
const l = new Laminar('<YOUR_PROJECT_API_KEY>');
|
|
27
27
|
const result = await l.run({
|
|
28
|
-
|
|
28
|
+
pipeline: 'my_pipeline_name',
|
|
29
29
|
inputs: {'input': [{'role': 'user', 'content': 'hello'}]},
|
|
30
30
|
env: {'OPENAI_API_KEY': 'sk-some-key'}, // optional
|
|
31
31
|
metadata: {'session_id': 'your_custom_session_id'}, // optional
|
|
@@ -41,92 +41,3 @@ Resulting in:
|
|
|
41
41
|
runId: '05383a95-d316-4391-a64b-06c54d12982a'
|
|
42
42
|
}
|
|
43
43
|
```
|
|
44
|
-
|
|
45
|
-
### Making calls to pipelines that run your own logic
|
|
46
|
-
|
|
47
|
-
If your pipeline contains tool call nodes, they will be able to call your local code.
|
|
48
|
-
The only difference is that you need to pass references
|
|
49
|
-
to the functions you want to call right into our SDK.
|
|
50
|
-
|
|
51
|
-
Tools must be functions that take in one object argument and perform
|
|
52
|
-
the ES5+ object destructuring magic, e.g. `foo({a, b})`. Tools must return
|
|
53
|
-
either a string or a list of chat messages
|
|
54
|
-
|
|
55
|
-
Please note that, if you specify tools, a bi-directional communication to Laminar API will be established.
|
|
56
|
-
This only works in Node execution context; browser context is not supported.
|
|
57
|
-
|
|
58
|
-
Example use:
|
|
59
|
-
|
|
60
|
-
```js
|
|
61
|
-
import { Laminar, NodeInput } from '@lmnr-ai/lmnr';
|
|
62
|
-
|
|
63
|
-
// make sure to setup arguments as object
|
|
64
|
-
const myTool = ({
|
|
65
|
-
arg1, arg2
|
|
66
|
-
}: {
|
|
67
|
-
arg1: string,
|
|
68
|
-
arg2: number
|
|
69
|
-
}): NodeInput => {
|
|
70
|
-
// this tool teaches LLMs the beauty of JavaScript!
|
|
71
|
-
return arg1 + arg2;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const l = new Laminar('<YOUR_PROJECT_API_KEY>');
|
|
75
|
-
const result = await l.run({
|
|
76
|
-
endpoint: 'my_endpoint_name',
|
|
77
|
-
inputs: {'input': [{'role': 'user', 'content': 'hello'}]},
|
|
78
|
-
env: {'OPENAI_API_KEY': 'sk-some-key'}, // optional
|
|
79
|
-
metadata: {'session_id': 'your_custom_session_id'}, // optional
|
|
80
|
-
// specify as many tools as needed,
|
|
81
|
-
// Each tool name must match tool node name in the pipeline
|
|
82
|
-
tools: [myTool],
|
|
83
|
-
});
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### LaminarRemoteDebugger
|
|
87
|
-
|
|
88
|
-
If your pipeline contains tool call nodes, they will be able to call your local code.
|
|
89
|
-
If you want to test them from the Laminar workshop in your browser, you can attach to your
|
|
90
|
-
locally running debugger.
|
|
91
|
-
|
|
92
|
-
### Step-by-step instructions to use `LaminarRemoteDebugger`:
|
|
93
|
-
|
|
94
|
-
#### 1. Create your pipeline with tool call nodes
|
|
95
|
-
|
|
96
|
-
Add tool calls to your pipeline; node names must match the functions you want to call.
|
|
97
|
-
|
|
98
|
-
#### 2. Start LaminarRemoteDebugger in your code
|
|
99
|
-
|
|
100
|
-
Example:
|
|
101
|
-
|
|
102
|
-
```js
|
|
103
|
-
import { LaminarRemoteDebugger, NodeInput } from '@lmnr-ai/lmnr';
|
|
104
|
-
|
|
105
|
-
// make sure to setup arguments as object
|
|
106
|
-
const myTool = ({
|
|
107
|
-
arg1, arg2
|
|
108
|
-
}: {
|
|
109
|
-
arg1: string,
|
|
110
|
-
arg2: number
|
|
111
|
-
}): NodeInput => {
|
|
112
|
-
// this tool teaches LLMs the beauty of JavaScript!
|
|
113
|
-
return arg1 + arg2;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const dbgr = new LaminarRemoteDebugger('<YOUR_PROJECT_API_KEY>', [myTool]);
|
|
117
|
-
dbgr.start();
|
|
118
|
-
// the session id will be printed to console.
|
|
119
|
-
// It is also returned from this promise, but you may not want to `await` it
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
This will establish a connection with Laminar API and allow for the pipeline execution
|
|
123
|
-
to call your local functions.
|
|
124
|
-
|
|
125
|
-
#### 3. Link lmnr.ai workshop to your debugger
|
|
126
|
-
|
|
127
|
-
Set up `DEBUGGER_SESSION_ID` environment variable in your pipeline.
|
|
128
|
-
|
|
129
|
-
#### 4. Run and experiment
|
|
130
|
-
|
|
131
|
-
You can run as many sessions as you need, experimenting with your flows.
|
|
132
|
-
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmnr-ai/lmnr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "TypeScript SDK for Laminar AI",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/lmnr-ai/ts
|
|
8
|
+
"url": "git+https://github.com/lmnr-ai/lmnr-ts.git"
|
|
9
9
|
},
|
|
10
10
|
"keywords": [
|
|
11
11
|
"laminar",
|
|
@@ -13,24 +13,21 @@
|
|
|
13
13
|
"sdk",
|
|
14
14
|
"lmnr.ai"
|
|
15
15
|
],
|
|
16
|
-
"author": "founders@
|
|
16
|
+
"author": "founders@lmnr.ai",
|
|
17
17
|
"license": "Apache-2.0",
|
|
18
18
|
"bugs": {
|
|
19
|
-
"url": "https://github.com/lmnr-ai/ts
|
|
20
|
-
},
|
|
21
|
-
"homepage": "https://github.com/lmnr-ai/ts-sdk#readme",
|
|
22
|
-
"dependencies": {
|
|
23
|
-
"@types/ws": "^8.5.10",
|
|
24
|
-
"browser-or-node": "^3.0.0",
|
|
25
|
-
"uuid": "^10.0.0",
|
|
26
|
-
"ws": "^8.18.0"
|
|
19
|
+
"url": "https://github.com/lmnr-ai/lmnr-ts/issues"
|
|
27
20
|
},
|
|
21
|
+
"homepage": "https://github.com/lmnr-ai/lmnr-ts#README",
|
|
28
22
|
"devDependencies": {
|
|
29
23
|
"@types/uuid": "^10.0.0",
|
|
30
24
|
"bufferutil": "^4.0.8",
|
|
31
25
|
"tsup": "^8.2.1",
|
|
32
26
|
"typescript": "^5.5.3"
|
|
33
27
|
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@types/node": "^22.5.0"
|
|
30
|
+
},
|
|
34
31
|
"scripts": {
|
|
35
32
|
"build": "tsup",
|
|
36
33
|
"test": "echo \"Error: no test specified\" && exit 1"
|
package/src/index.ts
CHANGED
|
@@ -1,39 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isBrowser, isNode } from 'browser-or-node';
|
|
3
|
-
import { WebSocket } from 'ws';
|
|
1
|
+
import { PipelineRunResponse, PipelineRunRequest } from './types';
|
|
4
2
|
|
|
5
|
-
export { NodeInput,
|
|
6
|
-
export { RemoteDebugger as LaminarRemoteDebugger } from './remote_debugger';
|
|
3
|
+
export { NodeInput, PipelineRunResponse, PipelineRunRequest, ChatMessage } from './types';
|
|
7
4
|
|
|
8
5
|
export class Laminar {
|
|
9
6
|
private readonly projectApiKey: string;
|
|
10
7
|
private readonly url: string;
|
|
11
|
-
private readonly
|
|
12
|
-
private readonly response: EndpointRunResponse | null = null;
|
|
8
|
+
private readonly response: PipelineRunResponse | null = null;
|
|
13
9
|
|
|
14
10
|
constructor(projectApiKey: string) {
|
|
15
|
-
this.projectApiKey = projectApiKey;
|
|
16
|
-
this.url = 'https://api.lmnr.ai/
|
|
17
|
-
this.ws_url = 'wss://api.lmnr.ai/v2/endpoint/ws'
|
|
11
|
+
this.projectApiKey = projectApiKey ?? process.env.LMNR_PROJECT_API_KEY;
|
|
12
|
+
this.url = 'https://api.lmnr.ai/v1/pipeline/run'
|
|
18
13
|
}
|
|
19
14
|
|
|
20
|
-
public async run(
|
|
15
|
+
public async run({
|
|
16
|
+
pipeline,
|
|
17
|
+
inputs,
|
|
18
|
+
env = {},
|
|
19
|
+
metadata = {},
|
|
20
|
+
}: PipelineRunRequest): Promise<PipelineRunResponse> {
|
|
21
21
|
if (this.projectApiKey === undefined) {
|
|
22
|
-
throw new Error(
|
|
22
|
+
throw new Error(
|
|
23
|
+
'Please initialize the Laminar object with your project API key ' +
|
|
24
|
+
'or set the LMNR_PROJECT_API_KEY environment variable'
|
|
25
|
+
);
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
if (request.tools !== undefined && request.tools.length > 0) {
|
|
26
|
-
return this._ws_run(request);
|
|
27
|
-
}
|
|
28
|
-
return this._run(request);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
private async _run({
|
|
32
|
-
endpoint,
|
|
33
|
-
inputs,
|
|
34
|
-
env = {},
|
|
35
|
-
metadata = {}
|
|
36
|
-
}: EndpointRunRequest): Promise<EndpointRunResponse> {
|
|
37
28
|
const response = await fetch(this.url, {
|
|
38
29
|
method: 'POST',
|
|
39
30
|
headers: {
|
|
@@ -42,87 +33,12 @@ export class Laminar {
|
|
|
42
33
|
},
|
|
43
34
|
body: JSON.stringify({
|
|
44
35
|
inputs,
|
|
45
|
-
|
|
46
|
-
env,
|
|
47
|
-
metadata
|
|
48
|
-
})
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
return await response.json() as EndpointRunResponse;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
private async _ws_run(request: EndpointRunRequest): Promise<EndpointRunResponse> {
|
|
55
|
-
if (isBrowser) {
|
|
56
|
-
throw new Error('Running tools in the browser is not supported. Please use the Node.js environment.');
|
|
57
|
-
} else if (isNode) {
|
|
58
|
-
return this._ws_run_node(request);
|
|
59
|
-
} else {
|
|
60
|
-
throw new Error('Unsupported environment');
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
private async _ws_run_node({
|
|
65
|
-
endpoint,
|
|
66
|
-
inputs,
|
|
67
|
-
env = {},
|
|
68
|
-
metadata = {},
|
|
69
|
-
tools = [],
|
|
70
|
-
}: EndpointRunRequest): Promise<EndpointRunResponse> {
|
|
71
|
-
const socket = new WebSocket(this.ws_url, [], {headers: {'Authorization': `Bearer ${this.projectApiKey}`}});
|
|
72
|
-
let response: EndpointRunResponse | null = null;
|
|
73
|
-
let reqId: string | null = null;
|
|
74
|
-
|
|
75
|
-
socket.on('open', () => {
|
|
76
|
-
socket.send(JSON.stringify({
|
|
77
|
-
inputs,
|
|
78
|
-
endpoint,
|
|
36
|
+
pipeline,
|
|
79
37
|
env,
|
|
80
38
|
metadata,
|
|
81
|
-
|
|
82
|
-
}));
|
|
83
|
-
});
|
|
84
|
-
socket.on('message', (data: NodeWebSocketMessage) => {
|
|
85
|
-
try {
|
|
86
|
-
const toolCall = JSON.parse(data.toString()) as ToolCallRequest;
|
|
87
|
-
reqId = toolCall.reqId;
|
|
88
|
-
const matchingTool = tools.find(tool => tool.name === toolCall.toolCall.function.name);
|
|
89
|
-
if (!matchingTool) {
|
|
90
|
-
throw new WebSocketError(
|
|
91
|
-
`Tool ${toolCall.toolCall.function.name} not found. ` +
|
|
92
|
-
`Registered tools: ${tools.map(tool => tool.name).join(', ')}`
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
let args = {}
|
|
96
|
-
try {
|
|
97
|
-
args = JSON.parse(toolCall.toolCall.function.arguments);
|
|
98
|
-
} catch (e) {}
|
|
99
|
-
try {
|
|
100
|
-
const result = matchingTool(args);
|
|
101
|
-
const toolResponse = {
|
|
102
|
-
reqId,
|
|
103
|
-
response: result
|
|
104
|
-
} as ToolCallResponse;
|
|
105
|
-
socket.send(JSON.stringify(toolResponse));
|
|
106
|
-
} catch (e) {
|
|
107
|
-
socket.send(JSON.stringify({
|
|
108
|
-
reqId,
|
|
109
|
-
error: (e as Error).message
|
|
110
|
-
} as ToolCallError));
|
|
111
|
-
}
|
|
112
|
-
} catch (e) {
|
|
113
|
-
if (e instanceof WebSocketError) {
|
|
114
|
-
throw e;
|
|
115
|
-
}
|
|
116
|
-
response = JSON.parse(data.toString()) as EndpointRunResponse;
|
|
117
|
-
socket.close();
|
|
118
|
-
}
|
|
39
|
+
})
|
|
119
40
|
});
|
|
120
41
|
|
|
121
|
-
|
|
122
|
-
while (!response) {
|
|
123
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return response;
|
|
42
|
+
return await response.json() as PipelineRunResponse;
|
|
127
43
|
}
|
|
128
|
-
}
|
|
44
|
+
}
|
package/src/tracing.ts
ADDED
|
File without changes
|
package/src/types.ts
CHANGED
|
@@ -7,57 +7,14 @@ export type NodeInput = ChatMessage[] | string;
|
|
|
7
7
|
|
|
8
8
|
export type Tool = (...args: any) => NodeInput | Promise<NodeInput>;
|
|
9
9
|
|
|
10
|
-
export type
|
|
10
|
+
export type PipelineRunRequest = {
|
|
11
11
|
inputs: Record<string, NodeInput>;
|
|
12
|
-
|
|
12
|
+
pipeline: string;
|
|
13
13
|
env?: Record<string, string>;
|
|
14
14
|
metadata?: Record<string, string>;
|
|
15
|
-
tools?: Tool[];
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
export type
|
|
17
|
+
export type PipelineRunResponse = {
|
|
19
18
|
outputs: Record<string, Record<string, NodeInput>>;
|
|
20
19
|
runId: string;
|
|
21
20
|
}
|
|
22
|
-
|
|
23
|
-
export type ToolCallRequest = {
|
|
24
|
-
reqId: string;
|
|
25
|
-
toolCall: ToolCall;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export type ToolFunctionCall = {
|
|
29
|
-
name: string;
|
|
30
|
-
arguments: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export type ToolCall = {
|
|
34
|
-
id: string;
|
|
35
|
-
type: 'function';
|
|
36
|
-
function: ToolFunctionCall;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type ToolCallResponse = {
|
|
40
|
-
reqId: string;
|
|
41
|
-
response: NodeInput;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export type NodeWebSocketMessage = {
|
|
45
|
-
type: 'utf8' | 'binary';
|
|
46
|
-
utf8Data?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export type RegisterDebuggerRequest = {
|
|
50
|
-
debuggerSessionId: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export type DeregisterDebuggerRequest = {
|
|
54
|
-
deregister: true;
|
|
55
|
-
debuggerSessionId: string;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export type ToolCallError = {
|
|
59
|
-
reqId: string;
|
|
60
|
-
error: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export class WebSocketError extends Error {}
|
package/src/remote_debugger.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DeregisterDebuggerRequest,
|
|
3
|
-
NodeWebSocketMessage,
|
|
4
|
-
RegisterDebuggerRequest,
|
|
5
|
-
Tool,
|
|
6
|
-
ToolCallRequest,
|
|
7
|
-
ToolCallError,
|
|
8
|
-
ToolCallResponse,
|
|
9
|
-
} from "./types";
|
|
10
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
11
|
-
import { WebSocket } from 'ws';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export class RemoteDebugger {
|
|
15
|
-
private readonly projectApiKey: string;
|
|
16
|
-
private readonly url: string = 'wss://api.lmnr.ai/v2/endpoint/ws';
|
|
17
|
-
private readonly tools: Tool[];
|
|
18
|
-
private sessionId: string | null = null;
|
|
19
|
-
private socket;
|
|
20
|
-
|
|
21
|
-
constructor(projectApiKey: string, tools: Tool[]) {
|
|
22
|
-
this.projectApiKey = projectApiKey;
|
|
23
|
-
this.tools = tools;
|
|
24
|
-
this.socket = new WebSocket(this.url, [], {headers: {'Authorization': `Bearer ${this.projectApiKey}`}});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
public async start(): Promise<string> {
|
|
28
|
-
this.sessionId = this.generateSessionId();
|
|
29
|
-
console.log(this.formatSessionIdAndRegisteredTools());
|
|
30
|
-
let reqId: string | null = null;
|
|
31
|
-
|
|
32
|
-
this.socket.on('open', () => {
|
|
33
|
-
this.socket.send(JSON.stringify({
|
|
34
|
-
debuggerSessionId: this.sessionId
|
|
35
|
-
} as RegisterDebuggerRequest));
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
this.socket.on('message', (data: NodeWebSocketMessage) => {
|
|
39
|
-
try {
|
|
40
|
-
const toolCall = JSON.parse(data.toString()) as ToolCallRequest;
|
|
41
|
-
reqId = toolCall.reqId;
|
|
42
|
-
const matchingTool = this.tools.find(tool => tool.name === toolCall.toolCall.function.name);
|
|
43
|
-
if (!matchingTool) {
|
|
44
|
-
const errMsg = `Tool ${toolCall.toolCall.function.name} not found. ` +
|
|
45
|
-
`Registered tools: ${this.tools.map(tool => tool.name).join(', ')}`;
|
|
46
|
-
console.error(errMsg);
|
|
47
|
-
this.socket.send(JSON.stringify({
|
|
48
|
-
reqId,
|
|
49
|
-
error: errMsg
|
|
50
|
-
} as ToolCallError));
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
let args = {}
|
|
54
|
-
try {
|
|
55
|
-
args = JSON.parse(toolCall.toolCall.function.arguments);
|
|
56
|
-
} catch (e) {}
|
|
57
|
-
try {
|
|
58
|
-
const result = matchingTool(args); // NodeInput
|
|
59
|
-
const toolResponse = {
|
|
60
|
-
reqId,
|
|
61
|
-
response: result
|
|
62
|
-
} as ToolCallResponse;
|
|
63
|
-
this.socket.send(JSON.stringify(toolResponse));
|
|
64
|
-
} catch (e) {
|
|
65
|
-
this.socket.send(JSON.stringify({
|
|
66
|
-
reqId,
|
|
67
|
-
error: (e as Error).message
|
|
68
|
-
} as ToolCallError));
|
|
69
|
-
}
|
|
70
|
-
} catch (e) {
|
|
71
|
-
console.error(`Received invalid message: ${data.toString()}`);
|
|
72
|
-
this.socket.send(JSON.stringify({
|
|
73
|
-
deregister: true,
|
|
74
|
-
debuggerSessionId: this.sessionId
|
|
75
|
-
} as RegisterDebuggerRequest));
|
|
76
|
-
this.socket.close();
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
return this.sessionId;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
public stop() {
|
|
84
|
-
this.socket.send(JSON.stringify({
|
|
85
|
-
deregister: true,
|
|
86
|
-
debuggerSessionId: this.sessionId
|
|
87
|
-
} as DeregisterDebuggerRequest));
|
|
88
|
-
this.socket.close();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
public getSessionId(): string | null {
|
|
92
|
-
return this.sessionId;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
private generateSessionId(): string {
|
|
96
|
-
return uuidv4();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
private formatSessionIdAndRegisteredTools(): string {
|
|
100
|
-
return `
|
|
101
|
-
========================================
|
|
102
|
-
Debugger Session ID:
|
|
103
|
-
${this.sessionId}
|
|
104
|
-
========================================
|
|
105
|
-
|
|
106
|
-
Registered functions:
|
|
107
|
-
${this.tools.map(tool => '- ' + tool.name).join(',\n')}
|
|
108
|
-
========================================
|
|
109
|
-
`
|
|
110
|
-
}
|
|
111
|
-
}
|