inlumen-agents 1.0.0b6.dev0__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 (63) hide show
  1. inlumen/__init__.py +1 -0
  2. inlumen/codegen/__init__.py +6 -0
  3. inlumen/codegen/context.py +322 -0
  4. inlumen/codegen/generation_budget.py +36 -0
  5. inlumen/codegen/generator.py +2887 -0
  6. inlumen/codegen/llm.py +418 -0
  7. inlumen/codegen/model.py +72 -0
  8. inlumen/codegen/model_plans.py +364 -0
  9. inlumen/codegen/multimodal.py +141 -0
  10. inlumen/codegen/pipeline_compiler.py +970 -0
  11. inlumen/codegen/resource_policy.py +288 -0
  12. inlumen/codegen/run.py +183 -0
  13. inlumen/codegen/sandbox.py +1678 -0
  14. inlumen/codegen/schemas.py +414 -0
  15. inlumen/codegen/task_profiles.py +802 -0
  16. inlumen/codegen/trusted_adapters.py +1197 -0
  17. inlumen/codegen/validation.py +187 -0
  18. inlumen/codegen/workspace.py +10 -0
  19. inlumen/contracts/__init__.py +25 -0
  20. inlumen/contracts/artifact_contract.py +135 -0
  21. inlumen/contracts/artifact_runtime.py +515 -0
  22. inlumen/contracts/connector_catalog.py +114 -0
  23. inlumen/contracts/graph_records.py +273 -0
  24. inlumen/contracts/map_scope.py +200 -0
  25. inlumen/contracts/model_plans.py +896 -0
  26. inlumen/contracts/node_definitions/__init__.py +11 -0
  27. inlumen/contracts/node_definitions/artifacts.py +92 -0
  28. inlumen/contracts/node_definitions/instance.py +212 -0
  29. inlumen/contracts/node_definitions/manifests/core.json +190 -0
  30. inlumen/contracts/node_definitions/node-definition.schema.json +123 -0
  31. inlumen/contracts/node_definitions/registry.py +103 -0
  32. inlumen/contracts/node_definitions/schema.py +141 -0
  33. inlumen/contracts/node_parameters.py +69 -0
  34. inlumen/contracts/node_ports.py +209 -0
  35. inlumen/contracts/pipeline_graph_validation.py +673 -0
  36. inlumen/contracts/plugin-manifest.schema.json +229 -0
  37. inlumen/contracts/plugin_manifest.py +364 -0
  38. inlumen/contracts/plugins.py +173 -0
  39. inlumen/contracts/port_types.json +310 -0
  40. inlumen/contracts/port_types.py +254 -0
  41. inlumen/contracts/project_ir.py +109 -0
  42. inlumen/contracts/step_types.py +115 -0
  43. inlumen/contracts/subpipeline_reference.py +650 -0
  44. inlumen/contracts/task_package_contract.py +304 -0
  45. inlumen/contracts/task_package_spec.json +211 -0
  46. inlumen/design/__init__.py +9 -0
  47. inlumen/design/catalog.py +47 -0
  48. inlumen/design/context.py +411 -0
  49. inlumen/design/contract.py +91 -0
  50. inlumen/design/guardrails.py +461 -0
  51. inlumen/design/memory_store.py +956 -0
  52. inlumen/design/prompt.py +206 -0
  53. inlumen/design/run.py +218 -0
  54. inlumen/design/store.py +211 -0
  55. inlumen/design/store_contract.py +268 -0
  56. inlumen/design/team.py +48 -0
  57. inlumen/design/tools.py +1140 -0
  58. inlumen/design/turn.py +114 -0
  59. inlumen_agents-1.0.0b6.dev0.dist-info/METADATA +106 -0
  60. inlumen_agents-1.0.0b6.dev0.dist-info/RECORD +63 -0
  61. inlumen_agents-1.0.0b6.dev0.dist-info/WHEEL +5 -0
  62. inlumen_agents-1.0.0b6.dev0.dist-info/licenses/LICENSE +202 -0
  63. inlumen_agents-1.0.0b6.dev0.dist-info/top_level.txt +1 -0
inlumen/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """inLUMEN's pipeline design and code generation agents, as a library."""
@@ -0,0 +1,6 @@
1
+ """inLUMEN's code generator: turns a pipeline graph into runnable Task packages.
2
+
3
+ Other programs call :func:`inlumen.codegen.run.generate`; the codegen service
4
+ calls :func:`inlumen.codegen.generator.generate_pipeline_script_bundles`
5
+ directly. Both run the same planning, generation, repair and validation.
6
+ """
@@ -0,0 +1,322 @@
1
+ """Build the code generator's input from a canvas graph.
2
+
3
+ The canvas graph is what the editor, ``inlumen.design.run.design()`` and the
4
+ Project JSON import produce. Where attached files live is the caller's
5
+ business: pass ``bucket_for`` to name a step's storage bucket and ``sample`` to
6
+ describe a file's contents (the inLUMEN backend reads MinIO; a library caller
7
+ can use :func:`directory_sampler`).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ import csv
13
+ import hashlib
14
+ import json
15
+ from collections.abc import Callable
16
+ from io import StringIO
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from inlumen.contracts.artifact_contract import classify_artifact
21
+ from inlumen.contracts.node_definitions.artifacts import implementation_plan_from_data
22
+ from inlumen.contracts.node_ports import normalize_node_ports
23
+ from inlumen.contracts.step_types import normalize_step_type
24
+ from inlumen.contracts.task_package_contract import TASK_METADATA_FILENAME
25
+
26
+ PIPELINE_CONTEXT_SCHEMA_VERSION = "inlumen.pipeline-script-generation-context@1"
27
+
28
+ DEFAULT_ALLOWED_PACKAGES = [
29
+ "pandas",
30
+ "numpy",
31
+ "pillow",
32
+ "scikit-learn",
33
+ "requests",
34
+ "pypdf",
35
+ "SpeechRecognition",
36
+ "pocketsphinx",
37
+ "textblob",
38
+ ]
39
+ CODEGEN_RUNTIME_FILENAMES = {
40
+ "main.py",
41
+ "requirements.txt",
42
+ TASK_METADATA_FILENAME,
43
+ "node-manifest.json",
44
+ "validation-report.json",
45
+ }
46
+ DEFAULT_SAMPLE_BINARY_MAX_BYTES = 16 * 1024 * 1024
47
+ SAMPLE_TEXT_MAX_CHARS = 12000
48
+
49
+ # (bucket, filename, entry) -> fields to merge into the file entry.
50
+ Sampler = Callable[[str, str, dict[str, Any]], dict[str, Any]]
51
+ BucketNamer = Callable[[str], str]
52
+
53
+
54
+ def local_bucket_name(flow_id: str) -> str:
55
+ """The bucket a step's files use in a local (single-workspace) inLUMEN."""
56
+ return f"files-step-id-{str(flow_id or '').strip().lower()}"
57
+
58
+
59
+ def node_data(node: dict[str, Any]) -> dict[str, Any]:
60
+ data = node.get("data")
61
+ return data if isinstance(data, dict) else node
62
+
63
+
64
+ def is_codegen_runtime_file(filename: str) -> bool:
65
+ """Whether a file is generated code rather than data the step reads."""
66
+ lower = str(filename or "").strip().lower()
67
+ return (
68
+ lower in CODEGEN_RUNTIME_FILENAMES
69
+ or lower.startswith("dockerfile.")
70
+ or lower.endswith((".py", ".pyi", ".sh", ".bash", ".js", ".mjs", ".cjs", ".ts", ".tsx"))
71
+ )
72
+
73
+
74
+ def sample_from_bytes(
75
+ content: bytes,
76
+ descriptor: dict[str, Any],
77
+ *,
78
+ binary_max_bytes: int = DEFAULT_SAMPLE_BINARY_MAX_BYTES,
79
+ ) -> dict[str, Any]:
80
+ """Describe a file's contents for the model and the validation sandbox.
81
+
82
+ Tables and JSON get columns and up to ten rows, text its first 12000
83
+ characters; binary files travel whole (base64) up to ``binary_max_bytes``.
84
+ """
85
+ kind = descriptor.get("kind")
86
+ file_format = descriptor.get("format")
87
+ if kind not in {"table", "json", "text"}:
88
+ if len(content) > binary_max_bytes:
89
+ return {
90
+ "size_bytes": len(content),
91
+ "sample_transport_error": (
92
+ f"Real input exceeds the {binary_max_bytes}-byte "
93
+ "binary validation transport limit."
94
+ ),
95
+ }
96
+ return {
97
+ "size_bytes": len(content),
98
+ "sample": {
99
+ "content_base64": base64.b64encode(content).decode("ascii"),
100
+ "content_sha256": hashlib.sha256(content).hexdigest(),
101
+ "omitted_bytes": 0,
102
+ },
103
+ }
104
+ text = content[:SAMPLE_TEXT_MAX_CHARS].decode("utf-8", errors="replace")
105
+ omitted = max(0, len(content) - len(content[:SAMPLE_TEXT_MAX_CHARS]))
106
+ if kind == "table" and file_format in {"csv", "tsv"}:
107
+ delimiter = "\t" if file_format == "tsv" else ","
108
+ try:
109
+ reader = csv.DictReader(StringIO(text), delimiter=delimiter)
110
+ rows = [row for _, row in zip(range(10), reader)]
111
+ columns = list(reader.fieldnames or [])
112
+ except Exception:
113
+ return {"sample": {"text": text, "omitted_bytes": omitted}}
114
+ return {
115
+ "columns": columns,
116
+ "sample": {
117
+ "rows": rows,
118
+ "omitted_bytes": omitted,
119
+ },
120
+ "size_bytes": len(content),
121
+ }
122
+ if kind == "json":
123
+ try:
124
+ parsed = json.loads(text)
125
+ except Exception:
126
+ parsed = None
127
+ rows = []
128
+ columns = []
129
+ if isinstance(parsed, list):
130
+ rows = [item for item in parsed[:10] if isinstance(item, dict)]
131
+ elif isinstance(parsed, dict):
132
+ candidate_rows = parsed.get("records") or parsed.get("items")
133
+ if isinstance(candidate_rows, list):
134
+ rows = [item for item in candidate_rows[:10] if isinstance(item, dict)]
135
+ if rows:
136
+ columns = sorted({key for row in rows for key in row})
137
+ return {
138
+ **({"columns": columns} if columns else {}),
139
+ "sample": {
140
+ **({"rows": rows} if rows else {"text": text}),
141
+ "omitted_bytes": omitted,
142
+ },
143
+ "size_bytes": len(content),
144
+ }
145
+ return {
146
+ "sample": {"text": text, "omitted_bytes": omitted},
147
+ "size_bytes": len(content),
148
+ }
149
+
150
+
151
+ def directory_sampler(
152
+ root: str | Path,
153
+ *,
154
+ binary_max_bytes: int = DEFAULT_SAMPLE_BINARY_MAX_BYTES,
155
+ ) -> Sampler:
156
+ """Sample attached files from a local folder.
157
+
158
+ A file attached to a step is looked up as ``root/<flow_id>/<filename>``
159
+ (bucket ``files-step-id-<flow_id>``), then as ``root/<filename>``. Missing
160
+ files are described by name only, as when storage is unavailable.
161
+ """
162
+ base = Path(root)
163
+
164
+ def sample(bucket: str, filename: str, entry: dict[str, Any]) -> dict[str, Any]:
165
+ flow_id = bucket.removeprefix("files-step-id-")
166
+ for candidate in (base / flow_id / filename, base / filename):
167
+ if candidate.is_file():
168
+ return sample_from_bytes(candidate.read_bytes(), entry, binary_max_bytes=binary_max_bytes)
169
+ return {}
170
+
171
+ return sample
172
+
173
+
174
+ def node_file_entries(
175
+ node: dict[str, Any],
176
+ *,
177
+ bucket_for: BucketNamer = local_bucket_name,
178
+ sample: Sampler | None = None,
179
+ include_runtime_artifacts: bool = False,
180
+ ) -> list[dict[str, Any]]:
181
+ """The data files attached to a step, sampled when ``sample`` is given."""
182
+ data = node_data(node)
183
+ raw_files = (
184
+ data.get("file_buckets")
185
+ if isinstance(data.get("file_buckets"), list)
186
+ else data.get("files")
187
+ )
188
+ if not isinstance(raw_files, list):
189
+ return []
190
+ flow_id = str(node.get("id") or data.get("flow_id") or data.get("id") or "").strip()
191
+ default_bucket = bucket_for(flow_id)
192
+ entries = []
193
+ for item in raw_files:
194
+ if isinstance(item, str):
195
+ filename = item.strip()
196
+ bucket = default_bucket
197
+ content_type = None
198
+ elif isinstance(item, dict):
199
+ filename = str(item.get("filename") or item.get("name") or "").strip()
200
+ bucket = str(item.get("bucket") or default_bucket).strip().lower()
201
+ content_type = item.get("content_type")
202
+ else:
203
+ continue
204
+ if not filename:
205
+ continue
206
+ if not include_runtime_artifacts and is_codegen_runtime_file(filename):
207
+ continue
208
+ entry = {
209
+ "filename": filename,
210
+ "bucket": bucket,
211
+ "content_type": content_type,
212
+ "role": item.get("role") if isinstance(item, dict) else None,
213
+ **classify_artifact(filename),
214
+ }
215
+ if isinstance(item, dict) and item.get("snapshot_bucket") and item.get("snapshot_object"):
216
+ entry["snapshot_bucket"] = item["snapshot_bucket"]
217
+ entry["snapshot_object"] = item["snapshot_object"]
218
+ if sample is not None:
219
+ entry.update(sample(
220
+ entry.get("snapshot_bucket", bucket), entry.get("snapshot_object", filename), entry
221
+ ))
222
+ entries.append(entry)
223
+ return entries
224
+
225
+
226
+ def node_descriptor(
227
+ node: dict[str, Any],
228
+ *,
229
+ bucket_for: BucketNamer = local_bucket_name,
230
+ sample: Sampler | None = None,
231
+ ) -> dict[str, Any]:
232
+ """One canvas step as the generator's ``NodeDescriptor`` (secrets left out)."""
233
+ data = node_data(node)
234
+ parameters = (
235
+ dict(data.get("param"))
236
+ if isinstance(data.get("param"), dict)
237
+ else {}
238
+ )
239
+ secret_parameter_names = {
240
+ str(name)
241
+ for name in (data.get("secret_params") or [])
242
+ if str(name).strip()
243
+ }
244
+ parameters = {
245
+ key: value
246
+ for key, value in parameters.items()
247
+ if str(key) not in secret_parameter_names
248
+ }
249
+ content = data.get("content")
250
+ if isinstance(content, str) and content.strip():
251
+ parameters["content"] = content.strip()
252
+ implementation = implementation_plan_from_data(data)
253
+ subpipeline = data.get("subpipeline")
254
+ if not isinstance(subpipeline, dict):
255
+ try:
256
+ subpipeline = json.loads(data.get("subpipeline_json") or "{}")
257
+ except (TypeError, ValueError):
258
+ subpipeline = {}
259
+ return {
260
+ "flow_id": str(node.get("id") or data.get("flow_id") or data.get("id") or ""),
261
+ "label": str(data.get("label") or ""),
262
+ "description": str(data.get("description") or ""),
263
+ "type": normalize_step_type(data.get("type")),
264
+ "template": str(data.get("template_label") or data.get("definition_id") or ""),
265
+ "parameters": parameters,
266
+ "secret_parameters": sorted(secret_parameter_names),
267
+ "implementation": implementation,
268
+ "subpipeline": subpipeline if isinstance(subpipeline, dict) else {},
269
+ "ports": normalize_node_ports(data.get("ports"), data.get("type")),
270
+ "files": node_file_entries(node, bucket_for=bucket_for, sample=sample),
271
+ }
272
+
273
+
274
+ def default_runtime_constraints() -> dict[str, Any]:
275
+ """The runtime inLUMEN generates pipelines for."""
276
+ return {
277
+ "language": "python",
278
+ "python_version": "3.11",
279
+ "base_image": "python:3.11-slim",
280
+ "allowed_packages": list(DEFAULT_ALLOWED_PACKAGES),
281
+ "allow_unlisted_model_packages": True,
282
+ "network_allowed": True,
283
+ "max_runtime_seconds": 900,
284
+ }
285
+
286
+
287
+ def pipeline_context(
288
+ graph: dict[str, Any],
289
+ *,
290
+ bucket_for: BucketNamer = local_bucket_name,
291
+ sample: Sampler | None = None,
292
+ ) -> dict[str, Any]:
293
+ """A canvas graph as a ``PipelineGenerationContext`` payload."""
294
+ nodes = graph.get("nodes") if isinstance(graph.get("nodes"), list) else []
295
+ edges = graph.get("edges") if isinstance(graph.get("edges"), list) else []
296
+ descriptors = [
297
+ node_descriptor(node, bucket_for=bucket_for, sample=sample)
298
+ for node in nodes
299
+ if isinstance(node, dict)
300
+ ]
301
+ return {
302
+ "schema_version": PIPELINE_CONTEXT_SCHEMA_VERSION,
303
+ "pipeline": graph.get("pipeline") if isinstance(graph.get("pipeline"), dict) else {},
304
+ "graph": {
305
+ "nodes": descriptors,
306
+ "edges": [
307
+ {
308
+ "source": str(edge.get("source")),
309
+ "target": str(edge.get("target")),
310
+ "source_port": str(
311
+ edge.get("sourceHandle") or edge.get("source_port") or ""
312
+ ),
313
+ "target_port": str(
314
+ edge.get("targetHandle") or edge.get("target_port") or ""
315
+ ),
316
+ }
317
+ for edge in edges
318
+ if isinstance(edge, dict) and edge.get("source") and edge.get("target")
319
+ ],
320
+ },
321
+ "runtime_constraints": default_runtime_constraints(),
322
+ }
@@ -0,0 +1,36 @@
1
+ """Request-scoped limits shared by generation and all nested repair calls."""
2
+ import asyncio
3
+ import functools
4
+ from contextvars import ContextVar
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass
9
+ class GenerationBudget:
10
+ max_requests: int
11
+ max_reported_cost_usd: float
12
+ requests: int = 0
13
+ reported_cost_usd: float = 0
14
+
15
+ def reserve(self):
16
+ if self.requests >= self.max_requests or self.reported_cost_usd >= self.max_reported_cost_usd:
17
+ raise ValueError('Generation budget exhausted. Review the partial result before retrying.')
18
+ self.requests += 1
19
+
20
+
21
+ CURRENT_BUDGET: ContextVar[GenerationBudget | None] = ContextVar('generation_budget', default=None)
22
+
23
+
24
+ def budgeted(function):
25
+ @functools.wraps(function)
26
+ async def wrapper(request, *args, **kwargs):
27
+ if CURRENT_BUDGET.get() is not None:
28
+ return await function(request, *args, **kwargs)
29
+ options = request.options
30
+ token = CURRENT_BUDGET.set(GenerationBudget(options.max_llm_requests, options.max_reported_cost_usd))
31
+ try:
32
+ async with asyncio.timeout(options.max_generation_seconds):
33
+ return await function(request, *args, **kwargs)
34
+ finally:
35
+ CURRENT_BUDGET.reset(token)
36
+ return wrapper