ivcap-lambda 0.7.22__tar.gz → 0.7.25__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,507 @@
1
+ Metadata-Version: 2.4
2
+ Name: ivcap-lambda
3
+ Version: 0.7.25
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: Python SDK for Lambda-Style IVCAP Services
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
+ `ivcap-lambda` is a Python library that provides the scaffolding for building **lambda-style services** on the [IVCAP platform](https://github.com/ivcap-works). It sits on top of [`ivcap-service`](https://pypi.org/project/ivcap-service/) and [FastAPI](https://fastapi.tiangolo.com/) and handles:
36
+
37
+ - Registering tool functions as HTTP endpoints (with async "try-later" semantics)
38
+ - Job execution in threads, result caching, and graceful shutdown
39
+ - Event/progress reporting back to the IVCAP platform
40
+ - Automatic tool-description endpoints (for AI agents and the MCP protocol)
41
+ - Optional OpenTelemetry tracing and an MCP endpoint
42
+
43
+ > **Template repository:** A ready-to-clone project template is available at
44
+ > [ivcap-works/ivcap-python-ai-tool-template](https://github.com/ivcap-works/ivcap-python-ai-tool-template).
45
+
46
+ ---
47
+
48
+ ## Contents
49
+
50
+ - [Installation](#installation)
51
+ - [Quick Start](#quick-start)
52
+ - [Defining a Tool](#defining-a-tool)
53
+ - [Request & Result models](#request--result-models)
54
+ - [The `@ivcap_lambda` decorator](#the-ivcap_lambda-decorator)
55
+ - [Accessing the Job Context](#accessing-the-job-context)
56
+ - [Async tools](#async-tools)
57
+ - [Starting the Server](#starting-the-server)
58
+ - [Endpoints Created per Tool](#endpoints-created-per-tool)
59
+ - [Reporting Progress Events](#reporting-progress-events)
60
+ - [Accessing IVCAP Artifacts](#accessing-ivcap-artifacts)
61
+ - [Project Layout & Configuration](#project-layout--configuration)
62
+ - [Running Locally](#running-locally)
63
+ - [Building & Deploying a Docker Image](#building--deploying-a-docker-image)
64
+ - [Migration from `ivcap-ai-tool`](#migration-from-ivcap-ai-tool)
65
+
66
+ ---
67
+
68
+ ## Installation
69
+
70
+ ```bash
71
+ pip install ivcap-lambda
72
+ ```
73
+
74
+ Or with Poetry:
75
+
76
+ ```bash
77
+ poetry add ivcap-lambda
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Quick Start
83
+
84
+ The simplest possible lambda service:
85
+
86
+ ```python
87
+ from pydantic import BaseModel, Field
88
+ from ivcap_service import Service, getLogger, with_schema
89
+ from ivcap_lambda import start_lambda_server, ivcap_lambda, ToolOptions, logging_init
90
+
91
+ logging_init()
92
+ logger = getLogger("my-service")
93
+
94
+ service = Service(
95
+ name="My IVCAP Service",
96
+ description="A minimal example service.",
97
+ contact={"name": "Alice", "email": "alice@example.com"},
98
+ license={"name": "MIT", "url": "https://opensource.org/licenses/MIT"},
99
+ )
100
+
101
+
102
+ @with_schema("urn:example:schema:echo.request.1")
103
+ class EchoRequest(BaseModel):
104
+ message: str = Field(..., description="The message to echo back.")
105
+
106
+
107
+ @with_schema("urn:example:schema:echo.1")
108
+ class EchoResult(BaseModel):
109
+ echo: str = Field(..., description="The echoed message.")
110
+
111
+
112
+ @ivcap_lambda("/", opts=ToolOptions(tags=["Echo"]))
113
+ def echo(req: EchoRequest) -> EchoResult:
114
+ """Echo a message
115
+
116
+ Returns the message passed in the request unchanged.
117
+ """
118
+ return EchoResult(echo=req.message)
119
+
120
+
121
+ if __name__ == "__main__":
122
+ start_lambda_server(service)
123
+ ```
124
+
125
+ Run it:
126
+
127
+ ```bash
128
+ python my_service.py --port 8090
129
+ ```
130
+
131
+ Test it:
132
+
133
+ ```bash
134
+ curl -X POST http://localhost:8090/ \
135
+ -H "content-type: application/json" \
136
+ -d '{"message": "Hello, IVCAP!"}'
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Defining a Tool
142
+
143
+ ### Request & Result models
144
+
145
+ Tool inputs and outputs are [Pydantic](https://docs.pydantic.dev/) `BaseModel` classes. Use the `@with_schema` decorator (from `ivcap_service`) to annotate them with an IVCAP schema URI — this adds a `$schema` field that the platform uses to identify payloads.
146
+
147
+ ```python
148
+ from pydantic import BaseModel, Field
149
+ from ivcap_service import with_schema
150
+
151
+ @with_schema("urn:example:schema:my-tool.request.1")
152
+ class MyRequest(BaseModel):
153
+ name: str = Field(..., description="Name to greet.")
154
+ count: int = Field(1, description="Number of times to repeat the greeting.", ge=1)
155
+
156
+ @with_schema("urn:example:schema:my-tool.1")
157
+ class MyResult(BaseModel):
158
+ greeting: str = Field(..., description="The generated greeting.")
159
+ ```
160
+
161
+ ### The `@ivcap_lambda` decorator
162
+
163
+ Use `@ivcap_lambda` to register a function as a tool endpoint:
164
+
165
+ ```python
166
+ from ivcap_lambda import ivcap_lambda, ToolOptions
167
+
168
+ @ivcap_lambda("/greet", opts=ToolOptions(tags=["Greeter"], service_id="/greet"))
169
+ def greet(req: MyRequest) -> MyResult:
170
+ """Greet a person
171
+
172
+ Generates a personalised greeting the requested number of times.
173
+ Describe your tool here — this text is surfaced to AI agents to
174
+ help them decide whether to use it.
175
+ """
176
+ return MyResult(greeting=(f"Hello, {req.name}! " * req.count).strip())
177
+ ```
178
+
179
+ **`ToolOptions` fields:**
180
+
181
+ | Field | Default | Description |
182
+ |---|---|---|
183
+ | `name` | (inferred from path) | Human-readable name for the tool endpoint |
184
+ | `tags` | (inferred from path) | OpenAPI tags for grouping endpoints |
185
+ | `max_wait_time` | `5.0` | Seconds the `POST` waits before returning `204 Try-Later` |
186
+ | `refresh_interval` | `3` | `Retry-Later` header value (seconds) returned with `204` |
187
+ | `service_id` | `None` | Overrides the service ID reported in the tool description |
188
+ | `post_route_opts` | `{}` | Additional kwargs forwarded to the FastAPI route constructor |
189
+ | `executor_opts` | `None` | `ExecutorOpts` (job cache size/TTL, thread pool size) |
190
+
191
+ **`service_id`:** If set to a path (e.g. `"/"` or `"/greet"`), the server prepends the public URL prefix automatically, so agents receive a fully-qualified service ID.
192
+
193
+ ### Accessing the Job Context
194
+
195
+ Your tool function can optionally accept a `JobContext` (from `ivcap_service`) as a keyword argument. The framework detects it by type annotation and injects it automatically. Through `JobContext` you can:
196
+
197
+ - Report progress events back to the platform
198
+ - Access IVCAP artifacts and other platform resources
199
+
200
+ ```python
201
+ from ivcap_service import JobContext
202
+ from fastapi import Request as FRequest
203
+
204
+ @ivcap_lambda("/process", opts=ToolOptions(tags=["Processor"]))
205
+ def process(req: MyRequest, freq: FRequest, jobCtxt: JobContext) -> MyResult:
206
+ """Process a request
207
+
208
+ Detailed description for agents.
209
+ """
210
+ logger.info(f"job_id={jobCtxt.job_id}")
211
+ with jobCtxt.report.step("work", "Starting work...") as step:
212
+ result = do_work(req)
213
+ step.finished(f"Finished in {len(result)} steps")
214
+ return MyResult(...)
215
+ ```
216
+
217
+ `JobContext` fields:
218
+
219
+ | Field | Type | Description |
220
+ |---|---|---|
221
+ | `job_id` | `str` | The unique job identifier (URN) |
222
+ | `report` | `EventReporter` | For emitting progress events to the platform |
223
+ | `job_authorization` | `str \| None` | Bearer token for authenticated calls |
224
+ | `ivcap` | `IVCAP` | IVCAP client for artifacts, services, etc. |
225
+
226
+ The `fastapi.Request` (`freq`) parameter is also optional and, like `JobContext`, is injected by type.
227
+
228
+ ### Async tools
229
+
230
+ Async functions are fully supported:
231
+
232
+ ```python
233
+ @ivcap_lambda("/async-greet", opts=ToolOptions(tags=["Greeter"]))
234
+ async def async_greet(req: MyRequest) -> MyResult:
235
+ """Greet asynchronously
236
+
237
+ Same as greet but runs in an async context.
238
+ """
239
+ await asyncio.sleep(0) # yield once
240
+ return MyResult(greeting=f"Hello, {req.name}!")
241
+ ```
242
+
243
+ ---
244
+
245
+ ## Starting the Server
246
+
247
+ ```python
248
+ if __name__ == "__main__":
249
+ start_lambda_server(service)
250
+ ```
251
+
252
+ `start_lambda_server` accepts:
253
+
254
+ | Argument | Description |
255
+ |---|---|
256
+ | `service` | A `Service` instance (name, contact, license) |
257
+ | `custom_args` | `Callable[[ArgumentParser], Namespace]` — add your own CLI flags |
258
+ | `run_opts` | Extra kwargs forwarded to `uvicorn.Config` |
259
+ | `with_telemetry` | `True` / `False` to force-enable or force-disable OpenTelemetry |
260
+
261
+ **Built-in CLI flags** (available to every service):
262
+
263
+ ```
264
+ --host HOST Bind address (default: 0.0.0.0 / $HOST)
265
+ --port PORT Port to listen on (default: 8090 / $PORT)
266
+ --with-telemetry Initialise OpenTelemetry tracing
267
+ --with-mcp Expose an MCP endpoint at /mcp
268
+ --print-tool-description Print the tool description JSON and exit
269
+ --print-service-description Print the full service description JSON and exit
270
+ ```
271
+
272
+ **Custom CLI flags example:**
273
+
274
+ ```python
275
+ def custom_args(parser: argparse.ArgumentParser) -> argparse.Namespace:
276
+ parser.add_argument("--my-flag", type=str, help="My custom flag")
277
+ args = parser.parse_args()
278
+ if args.my_flag:
279
+ os.environ["MY_FLAG"] = args.my_flag
280
+ return args
281
+
282
+ if __name__ == "__main__":
283
+ start_lambda_server(service, custom_args=custom_args)
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Endpoints Created per Tool
289
+
290
+ For each `@ivcap_lambda`-decorated function at path `{prefix}`, three routes are registered:
291
+
292
+ | Method | Path | Purpose |
293
+ |---|---|---|
294
+ | `POST` | `{prefix}` | Submit a job (execute the tool) |
295
+ | `GET` | `{prefix}` | Return a tool description (for agents / MCP) |
296
+ | `GET` | `/jobs/{job_id}` | Poll for the result of a deferred job |
297
+
298
+ Additionally, the framework registers:
299
+
300
+ - `GET /_healtz` — health check (returns `{"version": "..."}`)
301
+ - `GET /api` — Swagger/OpenAPI UI
302
+ - `GET /mcp` — MCP endpoint (only if `--with-mcp` is passed)
303
+
304
+ ### Asynchronous ("try-later") semantics
305
+
306
+ When a job takes longer than `ToolOptions.max_wait_time` (default 5 s), the `POST` returns **`204 No Content`** with:
307
+
308
+ ```
309
+ Location: /jobs/{job_id}
310
+ Retry-Later: 3
311
+ ```
312
+
313
+ The caller can then `GET /jobs/{job_id}` after the indicated delay to collect the result.
314
+
315
+ You can force asynchronous behaviour by sending `Prefer: respond-async` or control the timeout per request with a `Timeout: <seconds>` header.
316
+
317
+ ---
318
+
319
+ ## Reporting Progress Events
320
+
321
+ `JobContext.report` is an `EventReporter`. Use it to stream structured progress information to the IVCAP platform.
322
+
323
+ ```python
324
+ from ivcap_service.events import GenericEvent
325
+
326
+ # Structured step (emits a start event, then a finish event automatically)
327
+ with jobCtxt.report.step("download", "Downloading data...") as step:
328
+ for i, chunk in enumerate(data_stream):
329
+ process(chunk)
330
+ step.info(GenericEvent(name="progress", options={"chunk": i}))
331
+ step.finished(f"Downloaded {i+1} chunks")
332
+
333
+ # Emit a one-off event
334
+ jobCtxt.report.emit(GenericEvent(name="done", options={"count": 42}))
335
+ ```
336
+
337
+ **Available event types** (from `ivcap_service.events`):
338
+
339
+ | Class | Schema URI | Use |
340
+ |---|---|---|
341
+ | `GenericEvent(name, options)` | `urn:ivcap:schema:service.event.generic.1` | General-purpose named event |
342
+ | `GenericErrorEvent(error, context, stacktrace)` | `urn:ivcap:schema:service.event.error.1` | Error/exception reporting |
343
+
344
+ Custom events can be created by subclassing `BaseEvent` and defining a `SCHEMA` class variable.
345
+
346
+ ---
347
+
348
+ ## Accessing IVCAP Artifacts
349
+
350
+ `JobContext.ivcap` is an `IVCAP` client instance (from [`ivcap-client`](https://pypi.org/project/ivcap-client/)). Use it to interact with platform resources such as artifacts.
351
+
352
+ ```python
353
+ def my_tool(req: MyRequest, jobCtxt: JobContext) -> MyResult:
354
+ artifact = jobCtxt.ivcap.get_artifact(req.artifact_id)
355
+
356
+ with jobCtxt.report.step("download", f"Streaming {artifact.id}") as step:
357
+ bytes_received = 0
358
+ for chunk in artifact.as_stream(chunk_size=8192):
359
+ bytes_received += len(chunk)
360
+ step.info(GenericEvent(name="chunk", options={"bytes": bytes_received}))
361
+ step.finished(f"Downloaded {bytes_received} bytes")
362
+
363
+ return MyResult(size=bytes_received)
364
+ ```
365
+
366
+ ---
367
+
368
+ ## Project Layout & Configuration
369
+
370
+ A typical project looks like this:
371
+
372
+ ```
373
+ my-service/
374
+ ├── pyproject.toml
375
+ ├── my_service.py # tool implementation (entry point)
376
+ ├── Dockerfile
377
+ └── tests/
378
+ └── echo.json
379
+ ```
380
+
381
+ **`pyproject.toml`** — include the `ivcap` plugin section to integrate with the `poetry-plugin-ivcap` tooling:
382
+
383
+ ```toml
384
+ [project]
385
+ name = "my-service"
386
+ version = "0.1.0"
387
+ requires-python = ">=3.11"
388
+ dependencies = ["ivcap-lambda"]
389
+
390
+ [tool.poetry-plugin-ivcap]
391
+ service-file = "my_service.py" # entry-point script
392
+ service-id = "urn:ivcap:service:<uuid>" # stable service URN
393
+ service-type = "lambda"
394
+ port = 8095
395
+ ```
396
+
397
+ ---
398
+
399
+ ## Running Locally
400
+
401
+ ```bash
402
+ # Install dependencies
403
+ poetry install
404
+
405
+ # Run the service (uses port from pyproject.toml [tool.poetry-plugin-ivcap])
406
+ poetry ivcap run
407
+
408
+ # Or run directly
409
+ python my_service.py --port 8095
410
+ ```
411
+
412
+ **Quick test with `curl`:**
413
+
414
+ ```bash
415
+ # Synchronous call (waits up to max_wait_time)
416
+ curl -X POST http://localhost:8095/ \
417
+ -H "content-type: application/json" \
418
+ -d '{"message": "Hello!"}'
419
+
420
+ # Async call — get a 204 immediately with a Location header
421
+ curl -i -X POST http://localhost:8095/ \
422
+ -H "content-type: application/json" \
423
+ -H "Prefer: respond-async" \
424
+ -d '{"message": "Hello!"}'
425
+
426
+ # Collect the result (replace JOB_ID)
427
+ curl http://localhost:8095/jobs/JOB_ID
428
+ ```
429
+
430
+ **With IVCAP authentication:**
431
+
432
+ ```bash
433
+ curl -X POST http://localhost:8095/ \
434
+ -H "content-type: application/json" \
435
+ -H "job-id: urn:ivcap:job:<uuid>" \
436
+ -H "Authorization: Bearer $(ivcap context get access-token --refresh-token)" \
437
+ -d '{"message": "Hello!"}'
438
+ ```
439
+
440
+ **Print service/tool description (useful for IVCAP deployment):**
441
+
442
+ ```bash
443
+ python my_service.py --print-service-description
444
+ python my_service.py --print-tool-description
445
+ ```
446
+
447
+ ---
448
+
449
+ ## Building & Deploying a Docker Image
450
+
451
+ A minimal `Dockerfile`:
452
+
453
+ ```dockerfile
454
+ FROM python:3.11-slim-bookworm
455
+ WORKDIR /app
456
+ COPY pyproject.toml ./
457
+ RUN pip install poetry \
458
+ && poetry config virtualenvs.create false \
459
+ && poetry install --no-root \
460
+ && pip uninstall -y poetry
461
+
462
+ COPY my_service.py ./
463
+
464
+ ARG VERSION=???
465
+ ENV VERSION=$VERSION
466
+ ENV PORT=80
467
+
468
+ ENTRYPOINT ["python", "/app/my_service.py"]
469
+ ```
470
+
471
+ Build and run:
472
+
473
+ ```bash
474
+ docker build -t my-service .
475
+ docker run -p 8095:80 my-service
476
+ ```
477
+
478
+ ---
479
+
480
+ ## Migration from `ivcap-ai-tool`
481
+
482
+ | Old (deprecated) | New |
483
+ |---|---|
484
+ | `pip install ivcap-ai-tool` | `pip install ivcap-lambda` |
485
+ | `from ivcap_ai_tool import ...` | `from ivcap_lambda import ...` |
486
+ | `@ivcap_ai_tool(...)` | `@ivcap_lambda(...)` |
487
+
488
+ The old `ivcap-ai-tool` package is a compatibility shim that re-exports everything from `ivcap-lambda`. It will emit a `DeprecationWarning` at import time. No code changes beyond the import are required.
489
+
490
+ ---
491
+
492
+ ## Key Symbols
493
+
494
+ | Symbol | Package | Description |
495
+ |---|---|---|
496
+ | `ivcap_lambda` | `ivcap_lambda` | Decorator to register a tool function |
497
+ | `start_lambda_server` | `ivcap_lambda` | Start the FastAPI/uvicorn server |
498
+ | `start_tool_server` | `ivcap_lambda` | Deprecated alias for `start_lambda_server` |
499
+ | `ToolOptions` | `ivcap_lambda` | Options for `@ivcap_lambda` |
500
+ | `logging_init` | `ivcap_lambda` | Initialise structured logging |
501
+ | `Service` | `ivcap_service` | Service metadata (name, contact, license) |
502
+ | `JobContext` | `ivcap_service` | Per-job context (ID, reporter, IVCAP client) |
503
+ | `with_schema` | `ivcap_service` | Decorator to add a `$schema` URI to a model |
504
+ | `getLogger` | `ivcap_service` | Get a structured logger |
505
+ | `GenericEvent` | `ivcap_service.events` | Emit a named event |
506
+ | `GenericErrorEvent` | `ivcap_service.events` | Emit an error event |
507
+