ivcap-lambda 0.7.22__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.
@@ -0,0 +1,5 @@
1
+ # Initial version authors
2
+
3
+ * Max Ott <max.ott@csiro.au>
4
+
5
+ # Partial list of contributors
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2023, Commonwealth Scientific and Industrial Research Organisation (CSIRO) ABN 41 687 119 230
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ * Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: ivcap-lambda
3
+ Version: 0.7.22
4
+ Summary: Helper functions for building lambda-style services on the IVCAP platform
5
+ License-File: AUTHORS.md
6
+ License-File: LICENSE
7
+ Author: Max Ott
8
+ Author-email: max.ott@csiro.au
9
+ Requires-Python: >=3.11,<4.0
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Requires-Dist: cachetools (>=5.5.2,<6.0.0)
16
+ Requires-Dist: fastapi (>=0.121.2,<0.122.0)
17
+ Requires-Dist: ivcap-service (>=0.6.21,<0.7.0)
18
+ Requires-Dist: opentelemetry-instrumentation-fastapi (>=0.57b0)
19
+ Requires-Dist: uuid6 (==2024.7.10)
20
+ Requires-Dist: uvicorn (>=0.38.0,<0.39.0)
21
+ Description-Content-Type: text/markdown
22
+
23
+ # ivcap-lambda: A python library for building lambda-style services on the IVCAP platform
24
+
25
+ > **Package renamed:** `ivcap-ai-tool` has been renamed to `ivcap-lambda` to reflect that
26
+ > the library is useful for any lambda-style IVCAP service, not just AI agent tools.
27
+ > A [compatibility shim](./compat/) is published under the old name — existing apps
28
+ > will continue to work but will see a `DeprecationWarning` prompting migration.
29
+
30
+ <a href="https://scan.coverity.com/projects/ivcap-works-ivcap-ai-tool-sdk-python">
31
+ <img alt="Coverity Scan Build Status"
32
+ src="https://img.shields.io/coverity/scan/31491.svg"/>
33
+ </a>
34
+
35
+ A python library containing various helper and middleware functions
36
+ to simplify developing AI tools to be deployed on IVCAP.
37
+
38
+ > **Note:** A template git repositiory using this library can be found on github
39
+ [ivcap-works/ivcap-python-ai-tool-template](https://github.com/ivcap-works/ivcap-python-ai-tool-template). You may clone that and start from there.
40
+
41
+ ## Content
42
+
43
+ * [Register a Tool Function](#register)
44
+ * [Start the Service](#start)
45
+ * [JSON-RPC Middleware](#json-rpc)
46
+ * [Try-Later Middleware](#try-later)
47
+
48
+ ### Register a Tool Function <a name="register"></a>
49
+
50
+ ```python
51
+ class Request(BaseModel):
52
+ jschema: str = Field("urn:sd:schema:some_tool.request.1", alias="$schema")
53
+ ...
54
+
55
+ class Result(BaseModel):
56
+ jschema: str = Field("urn:sd:schema:some_tool.1", alias="$schema")
57
+ ...
58
+
59
+ def some_tool(req: Request) -> Result:
60
+ """
61
+ Here should go a quite extensive description of what the tool can be
62
+ used for so that an agent can work out if this tool is useful in
63
+ a specific context.
64
+
65
+ DO NOT ADD PARAMTER AND RETURN DECRIPTIONS -
66
+ DESCRIBE THEM IN THE `Request` MODEL
67
+ """
68
+ ...
69
+
70
+ return Result(...)
71
+
72
+ add_tool_api_route(app, "/", some_tool, opts=ToolOptions(tags=["Great Tool"]))
73
+ ```
74
+
75
+ ### Start the Service <a name="start"></a>
76
+
77
+ ```python
78
+ app = FastAPI(
79
+ ..
80
+ )
81
+
82
+ if __name__ == "__main__":
83
+ start_tool_server(app, some_tool)
84
+ ```
85
+
86
+ ### JSON-RPC Middleware <a name="json-rpc"></a>
87
+
88
+ This middleware will convert any `POST /` with a payload
89
+ following the [JSON-RPC](https://www.jsonrpc.org/specification)
90
+ specification to an internal `POST /{method}` and will return
91
+ the result formatted according to the JSON-RPC spec.
92
+
93
+ ```python
94
+ from ivcap_fastapi import use_json_rpc_middleware
95
+
96
+ app = FastAPI(
97
+ ..
98
+ )
99
+
100
+ use_json_rpc_middleware(app)
101
+ ```
102
+
103
+ ### Try-Later Middleware <a name="try-later"></a>
104
+
105
+ This middleware is supporting the use case where the execution of a
106
+ requested service is taking longer than the caller is willing to wait.
107
+ A typical use case is where the service is itself outsourcing the execution
108
+ to some other long-running service but may immediately receive a reference
109
+ to the eventual result.
110
+
111
+ In this case, raising a `TryLaterException` will return with a 204
112
+ status code and additional information on how to later check back for the
113
+ result.
114
+
115
+ ```python
116
+ from ivcap_fastapi import TryLaterException, use_try_later_middleware
117
+ use_try_later_middleware(app)
118
+
119
+ @app.post("/big_job")
120
+ def big_job(req: Request) -> Response:
121
+ jobID, expected_exec_time = scheduling_big_job(req)
122
+ raise TryLaterException(f"/big_job/jobs/{jobID}", expected_exec_time)
123
+
124
+ @app.get("/big_job/jobs/{jobID}")
125
+ def get_job(jobID: str) -> Response:
126
+ resp = find_result_for(job_id)
127
+ return resp
128
+ ```
129
+
130
+ Specifically, raising `TryLaterException(location, delay)` will
131
+ return an HTTP response with a 204 status code with the additional
132
+ HTTP headers `Location` and `Retry-Later` set to `location` and
133
+ `delay` respectively.
134
+
@@ -0,0 +1,111 @@
1
+ # ivcap-lambda: A python library for building lambda-style services on the IVCAP platform
2
+
3
+ > **Package renamed:** `ivcap-ai-tool` has been renamed to `ivcap-lambda` to reflect that
4
+ > the library is useful for any lambda-style IVCAP service, not just AI agent tools.
5
+ > A [compatibility shim](./compat/) is published under the old name — existing apps
6
+ > will continue to work but will see a `DeprecationWarning` prompting migration.
7
+
8
+ <a href="https://scan.coverity.com/projects/ivcap-works-ivcap-ai-tool-sdk-python">
9
+ <img alt="Coverity Scan Build Status"
10
+ src="https://img.shields.io/coverity/scan/31491.svg"/>
11
+ </a>
12
+
13
+ A python library containing various helper and middleware functions
14
+ to simplify developing AI tools to be deployed on IVCAP.
15
+
16
+ > **Note:** A template git repositiory using this library can be found on github
17
+ [ivcap-works/ivcap-python-ai-tool-template](https://github.com/ivcap-works/ivcap-python-ai-tool-template). You may clone that and start from there.
18
+
19
+ ## Content
20
+
21
+ * [Register a Tool Function](#register)
22
+ * [Start the Service](#start)
23
+ * [JSON-RPC Middleware](#json-rpc)
24
+ * [Try-Later Middleware](#try-later)
25
+
26
+ ### Register a Tool Function <a name="register"></a>
27
+
28
+ ```python
29
+ class Request(BaseModel):
30
+ jschema: str = Field("urn:sd:schema:some_tool.request.1", alias="$schema")
31
+ ...
32
+
33
+ class Result(BaseModel):
34
+ jschema: str = Field("urn:sd:schema:some_tool.1", alias="$schema")
35
+ ...
36
+
37
+ def some_tool(req: Request) -> Result:
38
+ """
39
+ Here should go a quite extensive description of what the tool can be
40
+ used for so that an agent can work out if this tool is useful in
41
+ a specific context.
42
+
43
+ DO NOT ADD PARAMTER AND RETURN DECRIPTIONS -
44
+ DESCRIBE THEM IN THE `Request` MODEL
45
+ """
46
+ ...
47
+
48
+ return Result(...)
49
+
50
+ add_tool_api_route(app, "/", some_tool, opts=ToolOptions(tags=["Great Tool"]))
51
+ ```
52
+
53
+ ### Start the Service <a name="start"></a>
54
+
55
+ ```python
56
+ app = FastAPI(
57
+ ..
58
+ )
59
+
60
+ if __name__ == "__main__":
61
+ start_tool_server(app, some_tool)
62
+ ```
63
+
64
+ ### JSON-RPC Middleware <a name="json-rpc"></a>
65
+
66
+ This middleware will convert any `POST /` with a payload
67
+ following the [JSON-RPC](https://www.jsonrpc.org/specification)
68
+ specification to an internal `POST /{method}` and will return
69
+ the result formatted according to the JSON-RPC spec.
70
+
71
+ ```python
72
+ from ivcap_fastapi import use_json_rpc_middleware
73
+
74
+ app = FastAPI(
75
+ ..
76
+ )
77
+
78
+ use_json_rpc_middleware(app)
79
+ ```
80
+
81
+ ### Try-Later Middleware <a name="try-later"></a>
82
+
83
+ This middleware is supporting the use case where the execution of a
84
+ requested service is taking longer than the caller is willing to wait.
85
+ A typical use case is where the service is itself outsourcing the execution
86
+ to some other long-running service but may immediately receive a reference
87
+ to the eventual result.
88
+
89
+ In this case, raising a `TryLaterException` will return with a 204
90
+ status code and additional information on how to later check back for the
91
+ result.
92
+
93
+ ```python
94
+ from ivcap_fastapi import TryLaterException, use_try_later_middleware
95
+ use_try_later_middleware(app)
96
+
97
+ @app.post("/big_job")
98
+ def big_job(req: Request) -> Response:
99
+ jobID, expected_exec_time = scheduling_big_job(req)
100
+ raise TryLaterException(f"/big_job/jobs/{jobID}", expected_exec_time)
101
+
102
+ @app.get("/big_job/jobs/{jobID}")
103
+ def get_job(jobID: str) -> Response:
104
+ resp = find_result_for(job_id)
105
+ return resp
106
+ ```
107
+
108
+ Specifically, raising `TryLaterException(location, delay)` will
109
+ return an HTTP response with a 204 status code with the additional
110
+ HTTP headers `Location` and `Retry-Later` set to `location` and
111
+ `delay` respectively.
@@ -0,0 +1,17 @@
1
+ #
2
+ # Copyright (c) 2023 Commonwealth Scientific and Industrial Research Organisation (CSIRO). All rights reserved.
3
+ # Use of this source code is governed by a BSD-style license that can be
4
+ # found in the LICENSE file. See the AUTHORS file for names of contributors.
5
+ #
6
+ """A library supporting the development of tools for agents to be deployed on IVCAP"""
7
+
8
+ from .version import __version__
9
+
10
+ from .server import start_tool_server
11
+ from .builder import add_tool_api_route, ToolOptions
12
+ from .executor import ExecutionContext, get_event_reporter, get_job_id
13
+ from .utils import get_public_url_prefix
14
+ from .secret import SecretMgrClient
15
+ from .decorators import ivcap_lambda
16
+ from .decorators import ivcap_lambda as ivcap_ai_tool # backward-compat alias
17
+ from .logger import logging_init
@@ -0,0 +1,260 @@
1
+ #
2
+ # Copyright (c) 2023 Commonwealth Scientific and Industrial Research Organisation (CSIRO). All rights reserved.
3
+ # Use of this source code is governed by a BSD-style license that can be
4
+ # found in the LICENSE file. See the AUTHORS file for names of contributors.
5
+ #
6
+ import asyncio
7
+ from dataclasses import dataclass
8
+ import json
9
+ from fastapi import FastAPI, Response, status, Request
10
+ from pydantic import BaseModel, Field
11
+ from typing import Any, Dict, List, Optional, Callable, Tuple, Type, TypeVar
12
+ from uuid6 import uuid6
13
+
14
+ from ivcap_service import getLogger, get_function_return_type, get_input_type, create_tool_definition
15
+ from ivcap_service import IvcapResult, ToolDefinition, ExecutionError
16
+
17
+ from .executor import ExecutionContext, Executor, ExecutorOpts
18
+ from .utils import get_title_from_path, get_public_url_prefix
19
+
20
+
21
+ class ErrorModel(BaseModel):
22
+ message: str
23
+ code: int
24
+
25
+ class ExecutionErrorModel(BaseModel):
26
+ jschema: str = Field("urn:ivcap:schema.ai-tool.error.1", alias="$schema")
27
+ message: str
28
+ traceback: str
29
+
30
+ JOB_URN_PREFIX = "urn:ivcap:job:"
31
+
32
+ logger = getLogger("wrapper")
33
+
34
+ class ToolOptions(BaseModel):
35
+ name: Optional[str] = Field(None, description="Name to be used for this tool")
36
+ tags: Optional[list[str]] = Field(None, description="OpenAPI tag for this set of functions")
37
+ max_wait_time: Optional[float] = Field(5.0, description="max. time in seconds to wait for result and before returning RetryLater")
38
+ refresh_interval: Optional[int] = Field(3, description="Time in seconds to wait before chacking again for a job result (used in RetryLater)")
39
+ executor_opts: Optional[ExecutorOpts] = Field(None, description="Options for the executor")
40
+ post_route_opts: Optional[Dict[str, Any]] = Field({}, description="Addtitional options given the POST route constructor")
41
+ service_id: Optional[str] = Field(None, description="overriding the default service id")
42
+
43
+ # Define a generic type for Pydantic models
44
+ T = TypeVar("T", bound=BaseModel)
45
+
46
+ WorkerFn = Callable[[BaseModel, Optional[ExecutionContext], Optional[Response]], BaseModel]
47
+
48
+ @dataclass
49
+ class ToolDescription:
50
+ name: str
51
+ path_prefix: str
52
+ worker_fn: WorkerFn
53
+ input: Tuple[Optional[Type[BaseModel]], Dict[str, Any]]
54
+ executor: Executor
55
+
56
+ tools: List[ToolDescription] = []
57
+
58
+ def add_tool_api_route(
59
+ app: FastAPI,
60
+ path_prefix: str,
61
+ worker_fn: WorkerFn,
62
+ *,
63
+ opts: Optional[ToolOptions] = ToolOptions(),
64
+ context: Optional[ExecutionContext] = None
65
+ ):
66
+ """Add a few routes to `app` for use with an AI tool.
67
+
68
+ The tool itself is implemented in `worker_fn` where the first
69
+ argument is a pydantic model describing all the tool's "public" parameters.
70
+ The function is also expected to return it's result as a single pydantic model.
71
+ The tool function can have two optioanl paramters, one with the same type as
72
+ `context` and the second one with `fastapi.Request`. The context paramter will
73
+ be identical to the above `context`, while the `request` will be the incoming
74
+ request.
75
+
76
+ This function then sets up three endpoints:
77
+ - POST {path_prefix}: To request the execution of the tool (the 'job')
78
+ - GET {path_prefix}/{job_id}: To collect the result of a tool execution
79
+ - GET {path_prefix}: To obtain a description of the tool suitable for most agent frameworks
80
+
81
+ The POST request will only wait `opts.max_wait_time` for the tool to finish. If it
82
+ hasn't finished by then, a `204 No Content` code will be returned with additional
83
+ header fields `Location` and `Retry-later` to inform the caller where and approx.
84
+ when the result can be collected later.
85
+
86
+ The `opts` parameter allows for customization of the endpoints. See `ToolOptions`
87
+ for a more detailed description.
88
+
89
+ Args:
90
+ app (FastAPI): The FastAPI context
91
+ path_prefix (str): The path prefix to use for this set of endpoints
92
+ worker_fn (Callable[[BaseModel, Optional[ExecutionContext], Optional[Response]], BaseModel]): _description_
93
+ opts (Optional[ToolOptions], optional): Additional behaviour settings. Defaults to ToolOptions().
94
+ context (Optional[ExecutionContext], optional): An optional context to be provided to every invocation of `worker_fn`. Defaults to None.
95
+ """
96
+ def_name, def_tag = get_title_from_path(path_prefix)
97
+ if opts.tags is None:
98
+ if def_tag == '':
99
+ def_tag = "Tool"
100
+ opts.tags = [def_tag]
101
+ if opts.name is None:
102
+ if def_name == '':
103
+ def_name = "Execute the tool"
104
+ opts.name = def_name
105
+
106
+ output_model = get_function_return_type(worker_fn)
107
+ executor = Executor[output_model](worker_fn, opts=opts.executor_opts, context=context)
108
+
109
+ tools.append(ToolDescription(name=worker_fn.__name__,
110
+ path_prefix=path_prefix,
111
+ worker_fn=worker_fn,
112
+ input=get_input_type(worker_fn),
113
+ executor=executor))
114
+
115
+ _add_do_job_route(app, path_prefix, worker_fn, executor, opts)
116
+ _add_get_job_route(app, path_prefix, worker_fn, executor, opts)
117
+ _add_get_tool_def_route(app, path_prefix, worker_fn, opts)
118
+
119
+ def _add_do_job_route(app: FastAPI, path_prefix: str, worker_fn: Callable, executor: Executor, opts: ToolOptions):
120
+ input_model, _ = get_input_type(worker_fn)
121
+ output_model = get_function_return_type(worker_fn)
122
+ summary, description = (worker_fn.__doc__.lstrip() + "\n").split("\n", 1)
123
+
124
+ async def route(data: input_model, req: Request) -> output_model: # type: ignore
125
+ job_id = req.headers.get("job-id")
126
+ if job_id == None:
127
+ job_id = str(uuid6())
128
+ elif job_id.startswith(JOB_URN_PREFIX):
129
+ job_id = job_id[len(JOB_URN_PREFIX):]
130
+
131
+ if req.headers.get("prefer") == "respond-async":
132
+ timeout = 0
133
+ else:
134
+ toh = req.headers.get("timeout")
135
+ if toh != None:
136
+ timeout = int(toh)
137
+ else:
138
+ timeout = opts.max_wait_time
139
+ logger.info(f"starting job {path_prefix}/jobs/{job_id} - timeout: {timeout} seconds")
140
+
141
+ queue = await executor.execute(data, job_id, req)
142
+ try:
143
+ el = await asyncio.wait_for(queue.get(), timeout=timeout)
144
+ queue.task_done()
145
+ el = _return_job_result(el, job_id)
146
+ return el
147
+ except asyncio.TimeoutError:
148
+ logger.info(f"... defer job result to later - {job_id}")
149
+ return _return_try_later(job_id, path_prefix, opts)
150
+
151
+ responses = {
152
+ 204: {
153
+ "headers": {
154
+ "Location": {
155
+ "description": "The URL where to pick up the result of this request",
156
+ "type": "string",
157
+ },
158
+ "Retry-Later": {
159
+ "description": "The time to wait before checking for a result",
160
+ "type": "integer",
161
+ },
162
+ },
163
+ },
164
+ 400: { "model": ErrorModel, },
165
+ # 400: {"model": Error}, 401: {"model": Error}, 429: {"model": Error}},
166
+ }
167
+ app.add_api_route(
168
+ path_prefix,
169
+ route,
170
+ # name=opts.name,
171
+ summary=summary,
172
+ description=description.strip(),
173
+ methods=["POST"],
174
+ responses=responses,
175
+ tags=opts.tags,
176
+ response_model_exclude_none=True,
177
+ response_model_by_alias=True,
178
+ **opts.post_route_opts,
179
+ )
180
+
181
+ def _add_get_job_route(app: FastAPI, path_prefix: str, worker_fn: Callable, executor: Executor, opts: ToolOptions):
182
+ output_model = get_function_return_type(worker_fn)
183
+ def route(job_id: str) -> output_model: # type: ignore
184
+ if job_id.startswith(JOB_URN_PREFIX):
185
+ job_id = job_id[len(JOB_URN_PREFIX):]
186
+ try:
187
+ result = executor.lookup_job(job_id)
188
+ if result == None:
189
+ return _return_try_later(job_id, path_prefix, opts)
190
+ return _return_job_result(result, job_id)
191
+ except KeyError:
192
+ return Response(status_code=status.HTTP_404_NOT_FOUND,
193
+ content=f"job {job_id} can't be found. It either never existed or its result is no longer cached.")
194
+
195
+ responses = {
196
+ 400: { "model": ErrorModel, },
197
+ }
198
+ path = "/jobs/" + "{job_id}"
199
+ if path_prefix != "/":
200
+ path = path_prefix + path
201
+ app.add_api_route(
202
+ path,
203
+ route,
204
+ summary="Returns the result of a particular job.",
205
+ methods=["GET"],
206
+ responses=responses,
207
+ tags=opts.tags,
208
+ response_model_exclude_none=True,
209
+ response_model_by_alias=True,
210
+ )
211
+
212
+ def _return_job_result(el, job_id):
213
+ h = { "job-id": JOB_URN_PREFIX + job_id }
214
+ if isinstance(el, IvcapResult):
215
+ return Response(status_code=status.HTTP_200_OK, content=el.content, media_type=el.content_type, headers=h)
216
+ elif isinstance(el, ExecutionError):
217
+ if el.type == ValueError:
218
+ m = ErrorModel(message=el.error, code=400)
219
+ status_code=status.HTTP_400_BAD_REQUEST
220
+ else:
221
+ m = ExecutionErrorModel(message=el.error, traceback=el.traceback)
222
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
223
+
224
+ return Response(status_code=status_code, content=m.model_dump_json(indent=2), media_type="application/json", headers=h)
225
+
226
+ msg = json.dumps({"error": f"please report unexpected internal error - unexpected result type {type(el)}"})
227
+ return Response(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=msg, media_type="application/json", headers=h)
228
+
229
+
230
+ def _add_get_tool_def_route(app: FastAPI, path_prefix: str, worker_fn: Callable, opts: ToolOptions):
231
+ async def route(req: Request) -> ToolDefinition: # type: ignore
232
+ service_id = opts.service_id
233
+ if service_id != None and service_id.startswith("/"):
234
+ # check if there is a forwarded header and prepand that
235
+ prefix = get_public_url_prefix(req)
236
+ service_id = f"{prefix}{service_id}"
237
+
238
+ return create_tool_definition(worker_fn, service_id=service_id)
239
+
240
+ app.add_api_route(
241
+ path_prefix,
242
+ route,
243
+ summary="Returns the description of this tool. Primarily used by agents.",
244
+ methods=["GET"],
245
+ tags=opts.tags,
246
+ response_model_exclude_none=True,
247
+ response_model_by_alias=True,
248
+ )
249
+
250
+
251
+ def _return_try_later(job_id: str, path_prefix: str, opts: ToolOptions):
252
+ location = f"/jobs/{job_id}"
253
+ if path_prefix != "/":
254
+ location = path_prefix + location
255
+ headers = {
256
+ "Location": location,
257
+ "Retry-Later": f"{opts.refresh_interval}",
258
+ "Ivcap-Self-Report-Result": "true"
259
+ }
260
+ return Response(status_code=status.HTTP_204_NO_CONTENT, headers=headers)
@@ -0,0 +1,60 @@
1
+ #
2
+ # Copyright (c) 2023 Commonwealth Scientific and Industrial Research Organisation (CSIRO). All rights reserved.
3
+ # Use of this source code is governed by a BSD-style license that can be
4
+ # found in the LICENSE file. See the AUTHORS file for names of contributors.
5
+ #
6
+ from typing import Optional
7
+
8
+ from .builder import ToolOptions, add_tool_api_route, WorkerFn
9
+ from .executor import ExecutionContext
10
+ from .server import get_fast_app
11
+
12
+
13
+ def ivcap_lambda(
14
+ path_prefix: str,
15
+ *,
16
+ opts: Optional[ToolOptions] = ToolOptions(),
17
+ context: Optional[ExecutionContext] = None,
18
+ ):
19
+ """Add a few routes to the service for use with an AI tool.
20
+
21
+ The tool itself is implemented in `worker_fn` where the first
22
+ argument is a pydantic model describing all the tool's "public" parameters.
23
+ The function is also expected to return it's result as a single pydantic model.
24
+ The tool function can have two optioanl paramters, one with the same type as
25
+ `context` and the second one with `fastapi.Request`. The context paramter will
26
+ be identical to the above `context`, while the `request` will be the incoming
27
+ request.
28
+
29
+ This function then sets up three endpoints:
30
+ - POST {path_prefix}: To request the execution of the tool (the 'job')
31
+ - GET {path_prefix}/{job_id}: To collect the result of a tool execution
32
+ - GET {path_prefix}: To obtain a description of the tool suitable for most agent frameworks
33
+
34
+ The POST request will only wait `opts.max_wait_time` for the tool to finish. If it
35
+ hasn't finished by then, a `204 No Content` code will be returned with additional
36
+ header fields `Location` and `Retry-later` to inform the caller where and approx.
37
+ when the result can be collected later.
38
+
39
+ The `opts` parameter allows for customization of the endpoints. See `ToolOptions`
40
+ for a more detailed description.
41
+
42
+ Args:
43
+ path_prefix (str): The path prefix to use for this set of endpoints
44
+ opts (Optional[ToolOptions], optional): Additional behaviour settings. Defaults to ToolOptions().
45
+ context (Optional[ExecutionContext], optional): An optional context to be provided to every invocation of `worker_fn`. Defaults to None.
46
+ """
47
+
48
+ def decorator(worker_fn: WorkerFn):
49
+ """
50
+ Args:
51
+ worker_fn (Callable[[BaseModel, Optional[ExecutionContext], Optional[Response]], BaseModel]): _description_
52
+ opts (Optional[ToolOptions], optional): Additional behaviour settings. Defaults to ToolOptions().
53
+ context (Optional[ExecutionContext], optional): An optional context to be provided to every invocation of `worker_fn`. Defaults to None.
54
+ """
55
+ add_tool_api_route(
56
+ get_fast_app(), path_prefix, worker_fn, opts=opts, context=context
57
+ )
58
+ return worker_fn
59
+
60
+ return decorator