agentic-workflow-orchestrator 1.0.0__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.
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentic-workflow-orchestrator
3
+ Version: 1.0.0
4
+ Summary: Orchestrate multi-agent workflows with autonomous reasoning, parallelism, rate limiting, and LLM integration
5
+ Home-page: https://github.com/org/orchestrator
6
+ Author: Manikandan Kasi
7
+ Author-email: Orchestrator Team <info@orchestrator.dev>
8
+ License: Apache License 2.0
9
+ Project-URL: Homepage, https://github.com/org/orchestrator
10
+ Project-URL: Documentation, https://github.com/org/orchestrator/blob/main/QUICKSTART.md
11
+ Project-URL: Repository, https://github.com/org/orchestrator
12
+ Project-URL: Issues, https://github.com/org/orchestrator/issues
13
+ Keywords: orchestration,workflow,multi-agent,agent,concurrency,rate-limiting,llm,mcp,asyncio
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: License :: OSI Approved :: Apache Software License
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Operating System :: OS Independent
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ Provides-Extra: llm-anthropic
27
+ Requires-Dist: anthropic>=0.7.0; extra == "llm-anthropic"
28
+ Provides-Extra: llm-openai
29
+ Requires-Dist: openai>=1.0.0; extra == "llm-openai"
30
+ Provides-Extra: llm-gemini
31
+ Requires-Dist: google-generativeai>=0.3.0; extra == "llm-gemini"
32
+ Provides-Extra: llm-xai
33
+ Requires-Dist: openai>=1.0.0; extra == "llm-xai"
34
+ Provides-Extra: llm-huggingface
35
+ Requires-Dist: transformers>=4.30.0; extra == "llm-huggingface"
36
+ Requires-Dist: torch>=2.0.0; extra == "llm-huggingface"
37
+ Provides-Extra: llm-azure
38
+ Requires-Dist: openai>=1.0.0; extra == "llm-azure"
39
+ Provides-Extra: llm
40
+ Requires-Dist: anthropic>=0.7.0; extra == "llm"
41
+ Requires-Dist: openai>=1.0.0; extra == "llm"
42
+ Requires-Dist: google-generativeai>=0.3.0; extra == "llm"
43
+ Requires-Dist: transformers>=4.30.0; extra == "llm"
44
+ Requires-Dist: torch>=2.0.0; extra == "llm"
45
+ Provides-Extra: engine
46
+ Provides-Extra: dev
47
+ Requires-Dist: pytest>=7.0; extra == "dev"
48
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
49
+ Requires-Dist: black>=23.0; extra == "dev"
50
+ Requires-Dist: isort>=5.12; extra == "dev"
51
+ Requires-Dist: mypy>=1.0; extra == "dev"
52
+ Dynamic: author
53
+ Dynamic: home-page
54
+ Dynamic: requires-python
55
+
56
+ # orchestrator (Python reference SDK)
57
+
58
+ In-process workflow orchestration engine + authoring API. Zero required dependencies
59
+ (stdlib `asyncio`); `pyyaml` is optional (for `.yaml` specs), `anthropic` optional (for the
60
+ real LLM adapter).
61
+
62
+ ## Concepts
63
+
64
+ - **Workflow** — a DAG of tasks loaded from a template (`Workflow.from_file/from_dict/from_json`).
65
+ - **Skill** — a registered handler `(ctx, inputs) -> output`, sync or `async`.
66
+ - **Tool** — a registered callable a skill invokes via `ctx.call_tool(...)`.
67
+ - **Orchestrator** — runs a workflow, enforcing concurrency + rate limits, and returns a `Report`.
68
+
69
+ ## Minimal usage
70
+
71
+ ```python
72
+ from orchestrator import Workflow, Orchestrator, Registry, MockLLM
73
+
74
+ reg = Registry()
75
+
76
+ @reg.tool("http.get")
77
+ def http_get(url):
78
+ return {"url": url, "ok": True}
79
+
80
+ @reg.skill("market.fetch")
81
+ def fetch(ctx, inputs):
82
+ ctx.call_tool("http.get", url=f"/prices/{inputs['ticker']}")
83
+ ctx.record_decision("selected primary feed", rationale="lowest latency")
84
+ return {"ticker": inputs["ticker"], "last": 199.4}
85
+
86
+ @reg.skill("llm.classify")
87
+ def classify(ctx, inputs):
88
+ r = ctx.llm(messages=[{"role": "user", "content": inputs["text"]}], model="claude-opus-4-8")
89
+ return r.text
90
+
91
+ wf = Workflow.from_file("../spec/examples/market-analysis.json")
92
+ report = Orchestrator(reg, llm=MockLLM()).run_sync(wf, inputs={"ticker": "AAPL"})
93
+ print(report.to_json())
94
+ ```
95
+
96
+ ## The `ctx` (SkillContext) API
97
+
98
+ | Member | Purpose |
99
+ |---|---|
100
+ | `ctx.inputs` | resolved task inputs |
101
+ | `ctx.call_tool(name, **kwargs)` | invoke a registered tool |
102
+ | `ctx.llm(messages, tools=, model=, **opts)` | call the configured LLM provider; usage is recorded |
103
+ | `ctx.record_decision(summary, rationale=, data=)` | append a critical decision to the trace |
104
+ | `ctx.log(msg)`, `ctx.attempt`, `ctx.cancelled` | logging / retry attempt / cooperative cancel flag |
105
+
106
+ ## Report
107
+
108
+ `report.to_dict()` / `report.to_json()` give: `status`, `duration_ms`, per-task
109
+ `{status, duration_ms, attempts, llm, decisions, error}`, `critical_path` (bottleneck chain),
110
+ `totals` (llm tokens, tool calls, retries), `errors`, and the resolved `output`.
111
+
112
+ ## Run
113
+
114
+ ```bash
115
+ python3 examples/run_market.py
116
+ python3 tests/test_engine.py
117
+ ```
118
+
119
+ ## Using a real LLM
120
+
121
+ ```python
122
+ from orchestrator import AnthropicProvider, Orchestrator
123
+ orch = Orchestrator(reg, llm=AnthropicProvider(model="claude-opus-4-8")) # needs ANTHROPIC_API_KEY + `pip install anthropic`
124
+ ```
125
+ Confirm current Claude model IDs/limits from the Claude API reference before pinning them.
@@ -0,0 +1,13 @@
1
+ orchestrator/__init__.py,sha256=iJnmq4UfaQuuSG3fsD1kZh7ZRfwpjnp69mwKrDOEUDs,1530
2
+ orchestrator/engine.py,sha256=t3yDrTYOAbVgNXn13dEq7MJY5Xl4c_13wyrGiFECmbg,10203
3
+ orchestrator/llm.py,sha256=V0jFx-JbhSVFutUxOycuYzfYeePvr5cZroP4OvgSeUo,13985
4
+ orchestrator/mcp_integration.py,sha256=X8-8anjlFUeCDUNYKRnfnTrIbalsOPsQCRvw4Fmu5C4,9600
5
+ orchestrator/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ orchestrator/ratelimit.py,sha256=iwJ016zE1d35Lml_Yt2e4wtW8wSHuBha4lH_IqpnFPg,2344
7
+ orchestrator/registry.py,sha256=RY54m6I2QFQPk9v-PHM0c7S0tnp-6nbrglr0uag0wKQ,3646
8
+ orchestrator/report.py,sha256=1Q9UYsDbJwGN4v7qw-kIAXtX4FHsKP6OZmtqFUvRLOU,3418
9
+ orchestrator/spec.py,sha256=VwdskmTyQdGawJughj6eq14x8CPB7DaNlMNi-jT1eFE,7627
10
+ agentic_workflow_orchestrator-1.0.0.dist-info/METADATA,sha256=7PSCQYvnO_UBuraBfCmYlxzX5Y6XW6biJVTRZh3_Tn0,4969
11
+ agentic_workflow_orchestrator-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ agentic_workflow_orchestrator-1.0.0.dist-info/top_level.txt,sha256=xxQ-0cX7ZiHS5jT83NoSu1MB43Iw3IGXRlLTZrI3QX4,13
13
+ agentic_workflow_orchestrator-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ orchestrator
@@ -0,0 +1,63 @@
1
+ """Multi-agent workflow orchestration SDK — Python reference implementation.
2
+
3
+ Quick start:
4
+
5
+ from orchestrator import Workflow, Orchestrator, Registry, MockLLM
6
+
7
+ reg = Registry()
8
+
9
+ @reg.skill("market.fetch")
10
+ def fetch(ctx, inputs):
11
+ ctx.record_decision("selected primary feed", rationale="lowest latency")
12
+ return {"ticker": inputs["ticker"], "last": 199.4}
13
+
14
+ wf = Workflow.from_file("spec/examples/market-analysis.json")
15
+ report = Orchestrator(reg, llm=MockLLM()).run_sync(wf, {"ticker": "AAPL"})
16
+ print(report.to_json())
17
+ """
18
+ from .engine import Orchestrator, TaskFailed
19
+ from .llm import (
20
+ AnthropicProvider,
21
+ AzureOpenAIProvider,
22
+ GeminiProvider,
23
+ HuggingFaceProvider,
24
+ LlmProvider,
25
+ LlmResult,
26
+ MockLLM,
27
+ OpenAIProvider,
28
+ XAIProvider,
29
+ )
30
+ from .ratelimit import TokenBucket
31
+ from .registry import Registry, SkillContext
32
+ from .report import Decision, LlmUsage, Report, TaskSpan, Totals
33
+ from .spec import RateLimit, Retry, SpecError, Task, Workflow, resolve_template
34
+
35
+ __all__ = [
36
+ "Orchestrator",
37
+ "TaskFailed",
38
+ "Registry",
39
+ "SkillContext",
40
+ "Workflow",
41
+ "Task",
42
+ "RateLimit",
43
+ "Retry",
44
+ "SpecError",
45
+ "resolve_template",
46
+ "Report",
47
+ "TaskSpan",
48
+ "Decision",
49
+ "LlmUsage",
50
+ "Totals",
51
+ "LlmProvider",
52
+ "LlmResult",
53
+ "MockLLM",
54
+ "AnthropicProvider",
55
+ "OpenAIProvider",
56
+ "GeminiProvider",
57
+ "XAIProvider",
58
+ "HuggingFaceProvider",
59
+ "AzureOpenAIProvider",
60
+ "TokenBucket",
61
+ ]
62
+
63
+ __version__ = "0.1.0"
orchestrator/engine.py ADDED
@@ -0,0 +1,245 @@
1
+ """The orchestration engine: DAG scheduler with concurrency + rate limiting.
2
+
3
+ This is the Python reference implementation. It runs workflows in-process and
4
+ defines the canonical scheduling/report semantics that the Go engine and other
5
+ SDKs must reproduce against the shared conformance fixtures.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import inspect
11
+ import random
12
+ import time
13
+ import uuid
14
+ from collections import defaultdict
15
+ from datetime import datetime, timezone
16
+ from typing import Any, Dict, List, Optional
17
+
18
+ from .llm import LlmProvider
19
+ from .ratelimit import NullBucket, bucket_for, token_bucket_for
20
+ from .registry import Registry, SkillContext
21
+ from .report import Report, TaskSpan, Totals, compute_critical_path
22
+ from .spec import Retry, Task, Workflow, resolve_template
23
+
24
+
25
+ class TaskFailed(Exception):
26
+ def __init__(self, task_id: str, message: str):
27
+ super().__init__(f"task {task_id!r} failed: {message}")
28
+ self.task_id = task_id
29
+ self.message = message
30
+
31
+
32
+ class Orchestrator:
33
+ """Embedded engine. Construct with a Registry and (optionally) an LLM provider."""
34
+
35
+ def __init__(self, registry: Registry, llm: Optional[LlmProvider] = None):
36
+ self.registry = registry
37
+ self.llm = llm
38
+
39
+ def run_sync(self, workflow: Workflow, inputs: Optional[Dict[str, Any]] = None) -> Report:
40
+ return asyncio.run(self.run(workflow, inputs))
41
+
42
+ async def run(self, workflow: Workflow, inputs: Optional[Dict[str, Any]] = None) -> Report:
43
+ inputs = inputs or {}
44
+ self._validate_inputs(workflow, inputs)
45
+ run_id = "r_" + uuid.uuid4().hex[:16]
46
+ started_wall = datetime.now(timezone.utc)
47
+ t0 = time.monotonic()
48
+
49
+ task_map = workflow.task_map()
50
+ children: Dict[str, List[str]] = defaultdict(list)
51
+ indeg: Dict[str, int] = {}
52
+ for t in workflow.tasks:
53
+ indeg[t.id] = len(t.depends_on)
54
+ for dep in t.depends_on:
55
+ children[dep].append(t.id)
56
+
57
+ # Rate buckets: one global, one per provider.
58
+ global_bucket = bucket_for(workflow.rate_limit, "global")
59
+ prov_req = {n: bucket_for(p.rate_limit, f"{n}.req") for n, p in workflow.providers.items()}
60
+ prov_tok = {n: token_bucket_for(p.rate_limit, f"{n}.tok") for n, p in workflow.providers.items()}
61
+
62
+ sem = asyncio.Semaphore(workflow.max_parallel)
63
+
64
+ results: Dict[str, Any] = {}
65
+ spans: Dict[str, TaskSpan] = {}
66
+ totals = Totals()
67
+
68
+ ready: List[str] = [t.id for t in workflow.tasks if indeg[t.id] == 0]
69
+ skipped: set = set()
70
+ running: Dict[asyncio.Task, str] = {}
71
+ run_failed = False
72
+ first_error: Optional[str] = None
73
+
74
+ def mark_descendants_skipped(start: str) -> None:
75
+ stack = list(children[start])
76
+ while stack:
77
+ cid = stack.pop()
78
+ if cid in skipped or cid in spans:
79
+ continue
80
+ skipped.add(cid)
81
+ spans[cid] = TaskSpan(id=cid, status="skipped")
82
+ stack.extend(children[cid])
83
+
84
+ while ready or running:
85
+ while ready:
86
+ tid = ready.pop(0)
87
+ if tid in skipped:
88
+ continue
89
+ coro = self._execute_task(
90
+ workflow, task_map[tid], run_id, results, sem,
91
+ global_bucket, prov_req, prov_tok,
92
+ )
93
+ running[asyncio.ensure_future(coro)] = tid
94
+
95
+ if not running:
96
+ break
97
+
98
+ done, _ = await asyncio.wait(running.keys(), return_when=asyncio.FIRST_COMPLETED)
99
+ for fut in done:
100
+ tid = running.pop(fut)
101
+ span: TaskSpan = fut.result() # _execute_task never raises
102
+ spans[tid] = span
103
+ totals.retries += max(0, span.attempts - 1)
104
+ if span.llm:
105
+ totals.llm_tokens_in += span.llm.tokens_in
106
+ totals.llm_tokens_out += span.llm.tokens_out
107
+ totals.tool_calls += getattr(span, "_tool_calls", 0)
108
+
109
+ if span.status == "succeeded":
110
+ results[tid] = span.output
111
+ for c in children[tid]:
112
+ indeg[c] -= 1
113
+ if indeg[c] == 0 and c not in skipped:
114
+ ready.append(c)
115
+ else: # failed
116
+ if first_error is None:
117
+ first_error = span.error
118
+ on_error = task_map[tid].on_error or workflow.default_on_error
119
+ if on_error == "fail_fast":
120
+ run_failed = True
121
+ for f in list(running.keys()):
122
+ f.cancel()
123
+ if running:
124
+ await asyncio.wait(running.keys())
125
+ running.clear()
126
+ ready.clear()
127
+ # Everything not yet terminal is skipped.
128
+ for other in task_map:
129
+ if other not in spans:
130
+ spans[other] = TaskSpan(id=other, status="skipped")
131
+ break
132
+ else:
133
+ mark_descendants_skipped(tid)
134
+ if run_failed:
135
+ break
136
+
137
+ depends_on = {t.id: t.depends_on for t in workflow.tasks}
138
+ status = "failed" if (run_failed or any(s.status == "failed" for s in spans.values())) else "succeeded"
139
+ output = None
140
+ if status == "succeeded" and workflow.output:
141
+ scope = {"input": inputs, **{tid: {"output": out} for tid, out in results.items()}}
142
+ try:
143
+ output = resolve_template(workflow.output, scope)
144
+ except Exception: # noqa: BLE001 - output is best-effort
145
+ output = None
146
+
147
+ ordered = [spans[t.id] for t in workflow.tasks if t.id in spans]
148
+ return Report(
149
+ workflow=workflow.name,
150
+ run_id=run_id,
151
+ status=status,
152
+ started_at=started_wall.isoformat(),
153
+ duration_ms=int((time.monotonic() - t0) * 1000),
154
+ tasks=ordered,
155
+ critical_path=compute_critical_path(spans, depends_on),
156
+ totals=totals,
157
+ errors=[first_error] if first_error else [],
158
+ output=output,
159
+ )
160
+
161
+ async def _execute_task(
162
+ self, workflow, task: Task, run_id, results, sem,
163
+ global_bucket, prov_req, prov_tok,
164
+ ) -> TaskSpan:
165
+ retry = task.retry or workflow.default_retry or Retry()
166
+ timeout = task.timeout if task.timeout is not None else workflow.default_timeout
167
+ # Resolve template refs against run inputs + outputs of completed deps.
168
+ scope = {"input": self._current_inputs, **{tid: {"output": out} for tid, out in results.items()}}
169
+ resolved_inputs = resolve_template(task.inputs, scope)
170
+
171
+ span = TaskSpan(id=task.id, status="failed", attempts=0)
172
+ t_task = time.monotonic()
173
+ last_err = "unknown error"
174
+
175
+ for attempt in range(1, retry.max + 2): # max retries => max+1 total attempts
176
+ span.attempts = attempt
177
+ ctx = SkillContext(
178
+ run_id=run_id, task_id=task.id, inputs=resolved_inputs, attempt=attempt,
179
+ registry=self.registry, llm_provider=self.llm,
180
+ on_tool_call=lambda: None,
181
+ )
182
+ try:
183
+ async with sem:
184
+ await global_bucket.acquire()
185
+ if task.provider:
186
+ await prov_req.get(task.provider, NullBucket()).acquire()
187
+ output = await self._invoke(task, ctx, timeout)
188
+ span.status = "succeeded"
189
+ span.output = output
190
+ span.decisions = ctx.decisions
191
+ span.llm = ctx.llm_usage
192
+ span._tool_calls = ctx.tool_calls # type: ignore[attr-defined]
193
+ span.duration_ms = int((time.monotonic() - t_task) * 1000)
194
+ return span
195
+ except asyncio.TimeoutError:
196
+ last_err = f"timeout after {timeout}s"
197
+ except asyncio.CancelledError:
198
+ span.status = "skipped"
199
+ span.error = "cancelled"
200
+ span.duration_ms = int((time.monotonic() - t_task) * 1000)
201
+ return span
202
+ except Exception as exc: # noqa: BLE001
203
+ last_err = f"{type(exc).__name__}: {exc}"
204
+ span.decisions = ctx.decisions
205
+ span.llm = ctx.llm_usage
206
+ span._tool_calls = ctx.tool_calls # type: ignore[attr-defined]
207
+ if attempt <= retry.max:
208
+ await asyncio.sleep(self._backoff(retry, attempt))
209
+
210
+ span.status = "failed"
211
+ span.error = last_err
212
+ span.duration_ms = int((time.monotonic() - t_task) * 1000)
213
+ return span
214
+
215
+ async def _invoke(self, task: Task, ctx: SkillContext, timeout: Optional[float]) -> Any:
216
+ handler = self.registry.get_skill(task.skill)
217
+ if inspect.iscoroutinefunction(handler):
218
+ coro = handler(ctx, ctx.inputs)
219
+ else:
220
+ loop = asyncio.get_event_loop()
221
+ coro = loop.run_in_executor(None, lambda: handler(ctx, ctx.inputs))
222
+ if timeout is not None:
223
+ return await asyncio.wait_for(coro, timeout=timeout)
224
+ return await coro
225
+
226
+ @staticmethod
227
+ def _backoff(retry: Retry, attempt: int) -> float:
228
+ if retry.backoff == "none":
229
+ base = 0.0
230
+ elif retry.backoff == "fixed":
231
+ base = retry.base
232
+ else: # exponential
233
+ base = retry.base * (2 ** (attempt - 1))
234
+ if retry.jitter and base > 0:
235
+ base *= 0.5 + random.random() / 2
236
+ return base
237
+
238
+ # --- inputs plumbing ---
239
+ _current_inputs: Dict[str, Any] = {}
240
+
241
+ def _validate_inputs(self, workflow: Workflow, inputs: Dict[str, Any]) -> None:
242
+ self._current_inputs = inputs
243
+ for name, schema in workflow.inputs_schema.items():
244
+ if schema.get("required") and name not in inputs:
245
+ raise ValueError(f"missing required input: {name!r}")