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.
- ivcap_lambda-0.7.22/AUTHORS.md +5 -0
- ivcap_lambda-0.7.22/LICENSE +29 -0
- ivcap_lambda-0.7.22/PKG-INFO +134 -0
- ivcap_lambda-0.7.22/README.md +111 -0
- ivcap_lambda-0.7.22/ivcap_lambda/__init__.py +17 -0
- ivcap_lambda-0.7.22/ivcap_lambda/builder.py +260 -0
- ivcap_lambda-0.7.22/ivcap_lambda/decorators.py +60 -0
- ivcap_lambda-0.7.22/ivcap_lambda/executor.py +292 -0
- ivcap_lambda-0.7.22/ivcap_lambda/logger.py +37 -0
- ivcap_lambda-0.7.22/ivcap_lambda/logging.json +77 -0
- ivcap_lambda-0.7.22/ivcap_lambda/mcp.py +261 -0
- ivcap_lambda-0.7.22/ivcap_lambda/py.typed +1 -0
- ivcap_lambda-0.7.22/ivcap_lambda/secret.py +44 -0
- ivcap_lambda-0.7.22/ivcap_lambda/server.py +217 -0
- ivcap_lambda-0.7.22/ivcap_lambda/service_definition.py +44 -0
- ivcap_lambda-0.7.22/ivcap_lambda/utils.py +100 -0
- ivcap_lambda-0.7.22/ivcap_lambda/version.py +22 -0
- ivcap_lambda-0.7.22/pyproject.toml +43 -0
|
@@ -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
|