ai-pipeline-core 0.2.6__py3-none-any.whl → 0.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. ai_pipeline_core/__init__.py +78 -125
  2. ai_pipeline_core/deployment/__init__.py +34 -0
  3. ai_pipeline_core/deployment/base.py +861 -0
  4. ai_pipeline_core/deployment/contract.py +80 -0
  5. ai_pipeline_core/deployment/deploy.py +561 -0
  6. ai_pipeline_core/deployment/helpers.py +97 -0
  7. ai_pipeline_core/deployment/progress.py +126 -0
  8. ai_pipeline_core/deployment/remote.py +116 -0
  9. ai_pipeline_core/docs_generator/__init__.py +54 -0
  10. ai_pipeline_core/docs_generator/__main__.py +5 -0
  11. ai_pipeline_core/docs_generator/cli.py +196 -0
  12. ai_pipeline_core/docs_generator/extractor.py +324 -0
  13. ai_pipeline_core/docs_generator/guide_builder.py +644 -0
  14. ai_pipeline_core/docs_generator/trimmer.py +35 -0
  15. ai_pipeline_core/docs_generator/validator.py +114 -0
  16. ai_pipeline_core/document_store/__init__.py +13 -0
  17. ai_pipeline_core/document_store/_summary.py +9 -0
  18. ai_pipeline_core/document_store/_summary_worker.py +170 -0
  19. ai_pipeline_core/document_store/clickhouse.py +492 -0
  20. ai_pipeline_core/document_store/factory.py +38 -0
  21. ai_pipeline_core/document_store/local.py +312 -0
  22. ai_pipeline_core/document_store/memory.py +85 -0
  23. ai_pipeline_core/document_store/protocol.py +68 -0
  24. ai_pipeline_core/documents/__init__.py +12 -14
  25. ai_pipeline_core/documents/_context_vars.py +85 -0
  26. ai_pipeline_core/documents/_hashing.py +52 -0
  27. ai_pipeline_core/documents/attachment.py +85 -0
  28. ai_pipeline_core/documents/context.py +128 -0
  29. ai_pipeline_core/documents/document.py +318 -1434
  30. ai_pipeline_core/documents/mime_type.py +37 -82
  31. ai_pipeline_core/documents/utils.py +4 -12
  32. ai_pipeline_core/exceptions.py +10 -62
  33. ai_pipeline_core/images/__init__.py +309 -0
  34. ai_pipeline_core/images/_processing.py +151 -0
  35. ai_pipeline_core/llm/__init__.py +6 -4
  36. ai_pipeline_core/llm/ai_messages.py +130 -81
  37. ai_pipeline_core/llm/client.py +327 -193
  38. ai_pipeline_core/llm/model_options.py +14 -86
  39. ai_pipeline_core/llm/model_response.py +60 -103
  40. ai_pipeline_core/llm/model_types.py +16 -34
  41. ai_pipeline_core/logging/__init__.py +2 -7
  42. ai_pipeline_core/logging/logging.yml +1 -1
  43. ai_pipeline_core/logging/logging_config.py +27 -37
  44. ai_pipeline_core/logging/logging_mixin.py +15 -41
  45. ai_pipeline_core/observability/__init__.py +32 -0
  46. ai_pipeline_core/observability/_debug/__init__.py +30 -0
  47. ai_pipeline_core/observability/_debug/_auto_summary.py +94 -0
  48. ai_pipeline_core/observability/_debug/_config.py +95 -0
  49. ai_pipeline_core/observability/_debug/_content.py +764 -0
  50. ai_pipeline_core/observability/_debug/_processor.py +98 -0
  51. ai_pipeline_core/observability/_debug/_summary.py +312 -0
  52. ai_pipeline_core/observability/_debug/_types.py +75 -0
  53. ai_pipeline_core/observability/_debug/_writer.py +843 -0
  54. ai_pipeline_core/observability/_document_tracking.py +146 -0
  55. ai_pipeline_core/observability/_initialization.py +194 -0
  56. ai_pipeline_core/observability/_logging_bridge.py +57 -0
  57. ai_pipeline_core/observability/_summary.py +81 -0
  58. ai_pipeline_core/observability/_tracking/__init__.py +6 -0
  59. ai_pipeline_core/observability/_tracking/_client.py +178 -0
  60. ai_pipeline_core/observability/_tracking/_internal.py +28 -0
  61. ai_pipeline_core/observability/_tracking/_models.py +138 -0
  62. ai_pipeline_core/observability/_tracking/_processor.py +158 -0
  63. ai_pipeline_core/observability/_tracking/_service.py +311 -0
  64. ai_pipeline_core/observability/_tracking/_writer.py +229 -0
  65. ai_pipeline_core/{tracing.py → observability/tracing.py} +139 -283
  66. ai_pipeline_core/pipeline/__init__.py +10 -0
  67. ai_pipeline_core/pipeline/decorators.py +915 -0
  68. ai_pipeline_core/pipeline/options.py +16 -0
  69. ai_pipeline_core/prompt_manager.py +16 -102
  70. ai_pipeline_core/settings.py +26 -31
  71. ai_pipeline_core/testing.py +9 -0
  72. ai_pipeline_core-0.4.1.dist-info/METADATA +807 -0
  73. ai_pipeline_core-0.4.1.dist-info/RECORD +76 -0
  74. {ai_pipeline_core-0.2.6.dist-info → ai_pipeline_core-0.4.1.dist-info}/WHEEL +1 -1
  75. ai_pipeline_core/documents/document_list.py +0 -420
  76. ai_pipeline_core/documents/flow_document.py +0 -112
  77. ai_pipeline_core/documents/task_document.py +0 -117
  78. ai_pipeline_core/documents/temporary_document.py +0 -74
  79. ai_pipeline_core/flow/__init__.py +0 -9
  80. ai_pipeline_core/flow/config.py +0 -483
  81. ai_pipeline_core/flow/options.py +0 -75
  82. ai_pipeline_core/pipeline.py +0 -718
  83. ai_pipeline_core/prefect.py +0 -63
  84. ai_pipeline_core/simple_runner/__init__.py +0 -14
  85. ai_pipeline_core/simple_runner/cli.py +0 -254
  86. ai_pipeline_core/simple_runner/simple_runner.py +0 -247
  87. ai_pipeline_core/storage/__init__.py +0 -8
  88. ai_pipeline_core/storage/storage.py +0 -628
  89. ai_pipeline_core/utils/__init__.py +0 -8
  90. ai_pipeline_core/utils/deploy.py +0 -373
  91. ai_pipeline_core/utils/remote_deployment.py +0 -269
  92. ai_pipeline_core-0.2.6.dist-info/METADATA +0 -500
  93. ai_pipeline_core-0.2.6.dist-info/RECORD +0 -41
  94. {ai_pipeline_core-0.2.6.dist-info → ai_pipeline_core-0.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,373 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Universal Prefect deployment script using Python API.
3
-
4
- This script:
5
- 1. Builds a Python package from pyproject.toml
6
- 2. Uploads it to Google Cloud Storage
7
- 3. Creates/updates a Prefect deployment using the RunnerDeployment pattern
8
-
9
- Requirements:
10
- - .env file with PREFECT_API_URL and optionally PREFECT_API_KEY
11
- - .env file with PREFECT_GCS_BUCKET
12
- - pyproject.toml with project name and version
13
- - Local package installed for flow metadata extraction
14
-
15
- Usage:
16
- python -m ai_pipeline_core.utils.deploy
17
- """
18
-
19
- import argparse
20
- import asyncio
21
- import os
22
- import subprocess
23
- import sys
24
- import tomllib
25
- import traceback
26
- from pathlib import Path
27
- from typing import Any, Optional
28
-
29
- from prefect.cli.deploy._storage import _PullStepStorage # type: ignore
30
- from prefect.client.orchestration import get_client
31
- from prefect.deployments.runner import RunnerDeployment
32
- from prefect.flows import load_flow_from_entrypoint
33
-
34
- from ai_pipeline_core.settings import settings
35
- from ai_pipeline_core.storage import Storage
36
-
37
- # ============================================================================
38
- # Configuration
39
- # ============================================================================
40
-
41
- WORK_POOL_NAME = settings.prefect_work_pool_name
42
- DEFAULT_WORK_QUEUE = settings.prefect_work_queue_name
43
- PREDEFINED_BUCKET = settings.prefect_gcs_bucket
44
-
45
- # ============================================================================
46
- # Deployer Class
47
- # ============================================================================
48
-
49
-
50
- class Deployer:
51
- """Deploy Prefect flows using the RunnerDeployment pattern.
52
-
53
- This is the official Prefect approach that handles flow registration,
54
- deployment creation/updates, and all edge cases automatically.
55
- """
56
-
57
- def __init__(self):
58
- """Initialize deployer."""
59
- self.config = self._load_config()
60
- self._setup_prefect_env()
61
-
62
- def _load_config(self) -> dict[str, Any]:
63
- """Load and normalize project configuration from pyproject.toml.
64
-
65
- Returns:
66
- Configuration dictionary with project metadata and deployment settings.
67
- """
68
- if not PREDEFINED_BUCKET:
69
- self._die(
70
- "PREFECT_GCS_BUCKET not found in .env file.\n"
71
- "Create a .env file with:\n"
72
- " PREFECT_GCS_BUCKET=your-bucket-name"
73
- )
74
-
75
- pyproject_path = Path("pyproject.toml")
76
- if not pyproject_path.exists():
77
- self._die("pyproject.toml not found. Run from project root.")
78
-
79
- with open(pyproject_path, "rb") as f:
80
- data = tomllib.load(f)
81
-
82
- project = data.get("project", {})
83
- name = project.get("name")
84
- version = project.get("version")
85
-
86
- if not name:
87
- self._die("Project name not found in pyproject.toml")
88
- if not version:
89
- self._die("Project version not found in pyproject.toml")
90
-
91
- # Normalize naming conventions
92
- # Hyphens in package names become underscores in Python imports
93
- package_name = name.replace("-", "_")
94
- flow_folder = name.replace("_", "-")
95
-
96
- return {
97
- "name": name,
98
- "package": package_name,
99
- "version": version,
100
- "bucket": PREDEFINED_BUCKET,
101
- "folder": f"flows/{flow_folder}",
102
- "tarball": f"{package_name}-{version}.tar.gz",
103
- "work_pool": WORK_POOL_NAME,
104
- "work_queue": DEFAULT_WORK_QUEUE,
105
- }
106
-
107
- def _setup_prefect_env(self):
108
- """Configure Prefect environment variables from .env file."""
109
- self.api_url = os.getenv("PREFECT_API_URL")
110
- if not self.api_url:
111
- self._die(
112
- "PREFECT_API_URL not found in .env file.\n"
113
- "Create a .env file with:\n"
114
- " PREFECT_API_URL=https://api.prefect.cloud/api/accounts/.../workspaces/..."
115
- )
116
-
117
- os.environ["PREFECT_API_URL"] = self.api_url
118
-
119
- # Optional: API key for authentication
120
- if api_key := os.getenv("PREFECT_API_KEY"):
121
- os.environ["PREFECT_API_KEY"] = api_key
122
-
123
- # Optional: Alternative auth method
124
- if api_auth := os.getenv("PREFECT_API_AUTH_STRING"):
125
- os.environ["PREFECT_API_AUTH_STRING"] = api_auth
126
-
127
- def _run(self, cmd: str, check: bool = True) -> Optional[str]:
128
- """Execute shell command and return output.
129
-
130
- Args:
131
- cmd: Shell command to execute
132
- check: Whether to raise on non-zero exit code
133
-
134
- Returns:
135
- Command stdout if successful, None if failed and check=False
136
- """
137
- result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
138
-
139
- if check and result.returncode != 0:
140
- self._die(f"Command failed: {cmd}\n{result.stderr}")
141
-
142
- return result.stdout.strip() if result.returncode == 0 else None
143
-
144
- def _info(self, msg: str):
145
- """Print info message."""
146
- print(f"→ {msg}")
147
-
148
- def _success(self, msg: str):
149
- """Print success message."""
150
- print(f"✓ {msg}")
151
-
152
- def _die(self, msg: str):
153
- """Print error and exit."""
154
- print(f"✗ {msg}", file=sys.stderr)
155
- sys.exit(1)
156
-
157
- def _build_package(self) -> Path:
158
- """Build Python package using `python -m build`.
159
-
160
- Returns:
161
- Path to the built tarball
162
- """
163
- self._info(f"Building {self.config['name']} v{self.config['version']}")
164
-
165
- # Build sdist (source distribution)
166
- build_cmd = "python -m build --sdist"
167
-
168
- self._run(build_cmd)
169
-
170
- # Verify tarball was created
171
- tarball_path = Path("dist") / self.config["tarball"]
172
- if not tarball_path.exists():
173
- self._die(
174
- f"Build artifact not found: {tarball_path}\n"
175
- f"Expected tarball name: {self.config['tarball']}\n"
176
- f"Check that pyproject.toml version matches."
177
- )
178
-
179
- self._success(f"Built {tarball_path.name} ({tarball_path.stat().st_size // 1024} KB)")
180
- return tarball_path
181
-
182
- async def _upload_package(self, tarball: Path):
183
- """Upload package tarball to Google Cloud Storage using Storage abstraction.
184
-
185
- Args:
186
- tarball: Path to the tarball to upload
187
- """
188
- # Extract flow_folder from the config folder path
189
- # e.g., "flows/ai-document-writer" -> "ai-document-writer"
190
- flow_folder = self.config["folder"].split("/", 1)[1] if "/" in self.config["folder"] else ""
191
-
192
- # Initialize storage with gs://bucket-name/flows and set subfolder to flow_folder
193
- base_uri = f"gs://{self.config['bucket']}/flows"
194
- storage = await Storage.from_uri(base_uri)
195
- storage = storage.with_base(flow_folder)
196
-
197
- dest_uri = storage.url_for(tarball.name)
198
- self._info(f"Uploading to {dest_uri}")
199
-
200
- # Read and upload the tarball
201
- tarball_bytes = tarball.read_bytes()
202
- await storage.write_bytes(tarball.name, tarball_bytes)
203
-
204
- self._success(f"Package uploaded to {self.config['folder']}/{tarball.name}")
205
-
206
- async def _deploy_via_api(self):
207
- """Create or update Prefect deployment using RunnerDeployment pattern.
208
-
209
- This is the official Prefect approach that:
210
- 1. Automatically creates/updates the flow registration
211
- 2. Handles deployment create vs update logic
212
- 3. Properly formats all parameters for the API
213
- """
214
- # Define entrypoint (assumes flow function has same name as package)
215
- entrypoint = f"{self.config['package']}:{self.config['package']}"
216
-
217
- # Load flow to get metadata
218
- # This requires the package to be installed locally (typical dev workflow)
219
- self._info(f"Loading flow from entrypoint: {entrypoint}")
220
- try:
221
- flow = load_flow_from_entrypoint(entrypoint)
222
- self._success(f"Loaded flow: {flow.name}")
223
- except ImportError as e:
224
- self._die(
225
- f"Failed to import flow: {e}\n\n"
226
- f"The package must be installed locally to extract flow metadata.\n"
227
- f"Install it with: pip install -e .\n\n"
228
- f"Expected entrypoint: {entrypoint}\n"
229
- f"This means: Python package '{self.config['package']}' "
230
- f"with flow function '{self.config['package']}'"
231
- )
232
- except AttributeError as e:
233
- self._die(
234
- f"Flow function not found: {e}\n\n"
235
- f"Expected flow function named '{self.config['package']}' "
236
- f"in package '{self.config['package']}'.\n"
237
- f"Check that your flow is decorated with @flow and named correctly."
238
- )
239
-
240
- # Define pull steps for workers
241
- # These steps tell workers how to get and install the flow code
242
- pull_steps = [
243
- {
244
- "prefect_gcp.deployments.steps.pull_from_gcs": {
245
- "id": "pull_code",
246
- "requires": "prefect-gcp>=0.6",
247
- "bucket": self.config["bucket"],
248
- "folder": self.config["folder"],
249
- }
250
- },
251
- {
252
- "prefect.deployments.steps.run_shell_script": {
253
- "id": "install_project",
254
- "stream_output": True,
255
- "directory": "{{ pull_code.directory }}",
256
- # Use uv for fast installation (worker has it installed)
257
- "script": f"uv pip install --system ./{self.config['tarball']}",
258
- }
259
- },
260
- ]
261
-
262
- # Create RunnerDeployment
263
- # This is the official Prefect pattern that handles all the complexity
264
- self._info(f"Creating deployment for flow '{flow.name}'")
265
-
266
- deployment = RunnerDeployment(
267
- name=self.config["package"],
268
- flow_name=flow.name,
269
- entrypoint=entrypoint,
270
- work_pool_name=self.config["work_pool"],
271
- work_queue_name=self.config["work_queue"],
272
- tags=[self.config["name"]],
273
- version=self.config["version"],
274
- description=flow.description
275
- or f"Deployment for {self.config['package']} v{self.config['version']}",
276
- storage=_PullStepStorage(pull_steps),
277
- parameters={},
278
- job_variables={},
279
- paused=False,
280
- )
281
-
282
- # Verify work pool exists before deploying
283
- async with get_client() as client:
284
- try:
285
- work_pool = await client.read_work_pool(self.config["work_pool"])
286
- self._success(
287
- f"Work pool '{self.config['work_pool']}' verified (type: {work_pool.type})"
288
- )
289
- except Exception as e:
290
- self._die(
291
- f"Work pool '{self.config['work_pool']}' not accessible: {e}\n"
292
- "Create it in the Prefect UI or with: prefect work-pool create"
293
- )
294
-
295
- # Apply deployment
296
- # This automatically handles create vs update based on whether deployment exists
297
- self._info("Applying deployment (create or update)...")
298
- try:
299
- deployment_id = await deployment.apply() # type: ignore
300
- self._success(f"Deployment ID: {deployment_id}")
301
-
302
- # Print helpful URLs
303
- if self.api_url:
304
- ui_url = self.api_url.replace("/api/", "/")
305
- print(f"\n🌐 View deployment: {ui_url}/deployments/deployment/{deployment_id}")
306
- print(f"🚀 Run now: prefect deployment run '{flow.name}/{self.config['package']}'")
307
- except Exception as e:
308
- self._die(f"Failed to apply deployment: {e}")
309
-
310
- async def run(self):
311
- """Execute the complete deployment pipeline."""
312
- print("=" * 70)
313
- print(f"Prefect Deployment: {self.config['name']} v{self.config['version']}")
314
- print(f"Target: gs://{self.config['bucket']}/{self.config['folder']}")
315
- print("=" * 70)
316
- print()
317
-
318
- # Phase 1: Build
319
- tarball = self._build_package()
320
-
321
- # Phase 2: Upload
322
- await self._upload_package(tarball)
323
-
324
- # Phase 3: Deploy
325
- await self._deploy_via_api()
326
-
327
- print()
328
- print("=" * 70)
329
- self._success("Deployment complete!")
330
- print("=" * 70)
331
-
332
-
333
- # ============================================================================
334
- # CLI Entry Point
335
- # ============================================================================
336
-
337
-
338
- def main():
339
- """Command-line interface for deployment script."""
340
- parser = argparse.ArgumentParser(
341
- description="Deploy Prefect flows to GCP using the official RunnerDeployment pattern",
342
- formatter_class=argparse.RawDescriptionHelpFormatter,
343
- epilog="""
344
- Example:
345
- python -m ai_pipeline_core.utils.deploy
346
-
347
- Prerequisites:
348
- - .env file with PREFECT_API_URL (and optionally PREFECT_API_KEY)
349
- - .env file with PREFECT_GCS_BUCKET
350
- - pyproject.toml with project name and version
351
- - Package installed locally: pip install -e .
352
- - GCP authentication configured (via service account or default credentials)
353
- - Work pool created in Prefect UI or CLI
354
- """,
355
- )
356
-
357
- parser.parse_args()
358
-
359
- try:
360
- deployer = Deployer()
361
- asyncio.run(deployer.run())
362
- except KeyboardInterrupt:
363
- print("\n✗ Deployment cancelled by user", file=sys.stderr)
364
- sys.exit(1)
365
- except Exception as e:
366
- print(f"\n✗ Unexpected error: {e}", file=sys.stderr)
367
-
368
- traceback.print_exc()
369
- sys.exit(1)
370
-
371
-
372
- if __name__ == "__main__":
373
- main()
@@ -1,269 +0,0 @@
1
- """Experimental remote deployment utilities.
2
-
3
- EXPERIMENTAL: This module provides utilities for calling remotely deployed Prefect flows.
4
- Subject to change in future versions.
5
- """
6
-
7
- import inspect
8
- from functools import wraps
9
- from typing import Any, Callable, ParamSpec, Type, TypeVar
10
-
11
- from prefect import get_client
12
- from prefect.client.orchestration import PrefectClient
13
- from prefect.client.schemas import FlowRun
14
- from prefect.context import AsyncClientContext
15
- from prefect.deployments.flow_runs import run_deployment
16
- from prefect.exceptions import ObjectNotFound
17
-
18
- from ai_pipeline_core import DocumentList, FlowDocument
19
- from ai_pipeline_core.settings import settings
20
- from ai_pipeline_core.tracing import TraceLevel, set_trace_cost, trace
21
-
22
- # --------------------------------------------------------------------------- #
23
- # Utility functions (copied from pipeline.py for consistency)
24
- # --------------------------------------------------------------------------- #
25
-
26
-
27
- def _callable_name(obj: Any, fallback: str) -> str:
28
- """Safely extract callable's name for error messages.
29
-
30
- Args:
31
- obj: Any object that might have a __name__ attribute.
32
- fallback: Default name if extraction fails.
33
-
34
- Returns:
35
- The callable's __name__ if available, fallback otherwise.
36
-
37
- Note:
38
- Internal helper that never raises exceptions.
39
- """
40
- try:
41
- n = getattr(obj, "__name__", None)
42
- return n if isinstance(n, str) else fallback
43
- except Exception:
44
- return fallback
45
-
46
-
47
- def _is_already_traced(func: Callable[..., Any]) -> bool:
48
- """Check if a function has already been wrapped by the trace decorator.
49
-
50
- This checks both for the explicit __is_traced__ marker and walks
51
- the __wrapped__ chain to detect nested trace decorations.
52
-
53
- Args:
54
- func: Function to check for existing trace decoration.
55
-
56
- Returns:
57
- True if the function is already traced, False otherwise.
58
- """
59
- # Check for explicit marker
60
- if hasattr(func, "__is_traced__") and func.__is_traced__: # type: ignore[attr-defined]
61
- return True
62
-
63
- # Walk the __wrapped__ chain to detect nested traces
64
- current = func
65
- depth = 0
66
- max_depth = 10 # Prevent infinite loops
67
-
68
- while hasattr(current, "__wrapped__") and depth < max_depth:
69
- wrapped = current.__wrapped__ # type: ignore[attr-defined]
70
- # Check if the wrapped function has the trace marker
71
- if hasattr(wrapped, "__is_traced__") and wrapped.__is_traced__: # type: ignore[attr-defined]
72
- return True
73
- current = wrapped
74
- depth += 1
75
-
76
- return False
77
-
78
-
79
- # --------------------------------------------------------------------------- #
80
- # Remote deployment execution
81
- # --------------------------------------------------------------------------- #
82
-
83
-
84
- async def run_remote_deployment(deployment_name: str, parameters: dict[str, Any]) -> Any:
85
- """Run a remote Prefect deployment.
86
-
87
- Args:
88
- deployment_name: Name of the deployment to run.
89
- parameters: Parameters to pass to the deployment.
90
-
91
- Returns:
92
- Result from the deployment execution.
93
-
94
- Raises:
95
- ValueError: If deployment is not found in local or remote Prefect API.
96
- """
97
-
98
- async def _run(client: PrefectClient, as_subflow: bool) -> Any:
99
- fr: FlowRun = await run_deployment(
100
- client=client, name=deployment_name, parameters=parameters, as_subflow=as_subflow
101
- ) # type: ignore
102
- return await fr.state.result() # type: ignore
103
-
104
- async with get_client() as client:
105
- try:
106
- await client.read_deployment_by_name(name=deployment_name)
107
- return await _run(client, True)
108
- except ObjectNotFound:
109
- pass
110
-
111
- if not settings.prefect_api_url:
112
- raise ValueError(f"{deployment_name} deployment not found, PREFECT_API_URL is not set")
113
-
114
- async with PrefectClient(
115
- api=settings.prefect_api_url,
116
- api_key=settings.prefect_api_key,
117
- auth_string=settings.prefect_api_auth_string,
118
- ) as client:
119
- try:
120
- await client.read_deployment_by_name(name=deployment_name)
121
- with AsyncClientContext.model_construct(
122
- client=client, _httpx_settings=None, _context_stack=0
123
- ):
124
- return await _run(client, False)
125
- except ObjectNotFound:
126
- pass
127
-
128
- raise ValueError(f"{deployment_name} deployment not found")
129
-
130
-
131
- P = ParamSpec("P")
132
- T = TypeVar("T")
133
-
134
-
135
- def remote_deployment(
136
- output_document_type: Type[FlowDocument],
137
- *,
138
- # tracing
139
- name: str | None = None,
140
- trace_level: TraceLevel = "always",
141
- trace_ignore_input: bool = False,
142
- trace_ignore_output: bool = False,
143
- trace_ignore_inputs: list[str] | None = None,
144
- trace_input_formatter: Callable[..., str] | None = None,
145
- trace_output_formatter: Callable[..., str] | None = None,
146
- trace_cost: float | None = None,
147
- trace_trim_documents: bool = True,
148
- ) -> Callable[[Callable[P, T]], Callable[P, T]]:
149
- """Decorator for calling remote Prefect deployments with automatic tracing.
150
-
151
- EXPERIMENTAL: Decorator for calling remote Prefect deployments with automatic
152
- parameter serialization, result deserialization, and LMNR tracing.
153
-
154
- IMPORTANT: Never combine with @trace decorator - this includes tracing automatically.
155
- The framework will raise TypeError if you try to use both decorators together.
156
-
157
- Best Practice - Use Defaults:
158
- For most use cases, only specify output_document_type. The defaults provide
159
- automatic tracing with optimal settings.
160
-
161
- Args:
162
- output_document_type: The FlowDocument type to deserialize results into.
163
- name: Custom trace name (defaults to function name).
164
- trace_level: When to trace ("always", "debug", "off").
165
- - "always": Always trace (default)
166
- - "debug": Only trace when LMNR_DEBUG="true"
167
- - "off": Disable tracing
168
- trace_ignore_input: Don't trace input arguments.
169
- trace_ignore_output: Don't trace return value.
170
- trace_ignore_inputs: List of parameter names to exclude from tracing.
171
- trace_input_formatter: Custom formatter for input tracing.
172
- trace_output_formatter: Custom formatter for output tracing.
173
- trace_cost: Optional cost value to track in metadata. When provided and > 0,
174
- sets gen_ai.usage.output_cost, gen_ai.usage.cost, and cost metadata.
175
- trace_trim_documents: Trim document content in traces to first 100 chars (default True).
176
- Reduces trace size with large documents.
177
-
178
- Returns:
179
- Decorator function that wraps the target function.
180
-
181
- Example:
182
- >>> # RECOMMENDED - Minimal usage
183
- >>> @remote_deployment(output_document_type=OutputDoc)
184
- >>> async def process_remotely(
185
- ... project_name: str,
186
- ... documents: DocumentList,
187
- ... flow_options: FlowOptions
188
- >>> ) -> DocumentList:
189
- ... pass # This stub is replaced by remote call
190
- >>>
191
- >>> # With custom tracing
192
- >>> @remote_deployment(
193
- ... output_document_type=OutputDoc,
194
- ... trace_cost=0.05, # Track cost of remote execution
195
- ... trace_level="debug" # Only trace in debug mode
196
- >>> )
197
- >>> async def debug_remote_flow(...) -> DocumentList:
198
- ... pass
199
-
200
- Note:
201
- - Remote calls are automatically traced with LMNR
202
- - The decorated function's body is never executed - it serves as a signature template
203
- - Deployment name is auto-derived from function name
204
- - DocumentList parameters are automatically serialized/deserialized
205
-
206
- Raises:
207
- TypeError: If function is already decorated with @trace.
208
- ValueError: If deployment is not found.
209
- """
210
-
211
- def decorator(func: Callable[P, T]) -> Callable[P, T]:
212
- fname = _callable_name(func, "remote_deployment")
213
-
214
- # Check if function is already traced
215
- if _is_already_traced(func):
216
- raise TypeError(
217
- f"@remote_deployment target '{fname}' is already decorated "
218
- f"with @trace. Remove the @trace decorator - @remote_deployment includes "
219
- f"tracing automatically."
220
- )
221
-
222
- @wraps(func)
223
- async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
224
- sig = inspect.signature(func)
225
- bound = sig.bind(*args, **kwargs)
226
- bound.apply_defaults()
227
-
228
- # Serialize parameters, converting DocumentList to list[dict]
229
- parameters = {}
230
- for pname, value in bound.arguments.items():
231
- if isinstance(value, DocumentList):
232
- parameters[pname] = [doc for doc in value]
233
- else:
234
- parameters[pname] = value
235
-
236
- # Auto-derive deployment name
237
- deployment_name = f"{func.__name__.replace('_', '-')}/{func.__name__}"
238
-
239
- result = await run_remote_deployment(
240
- deployment_name=deployment_name, parameters=parameters
241
- )
242
-
243
- # Set trace cost if provided
244
- if trace_cost is not None and trace_cost > 0:
245
- set_trace_cost(trace_cost)
246
-
247
- assert isinstance(result, list), "Result must be a list"
248
-
249
- # Auto-handle return type conversion from list[dict] to DocumentList
250
- return_type = sig.return_annotation
251
-
252
- assert return_type is DocumentList, "Return type must be a DocumentList"
253
- return DocumentList([output_document_type(**item) for item in result]) # type: ignore
254
-
255
- # Apply trace decorator
256
- traced_wrapper = trace(
257
- level=trace_level,
258
- name=name or fname,
259
- ignore_input=trace_ignore_input,
260
- ignore_output=trace_ignore_output,
261
- ignore_inputs=trace_ignore_inputs,
262
- input_formatter=trace_input_formatter,
263
- output_formatter=trace_output_formatter,
264
- trim_documents=trace_trim_documents,
265
- )(_wrapper)
266
-
267
- return traced_wrapper # type: ignore
268
-
269
- return decorator