flowforge-map 0.1.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.
- flowforge/__init__.py +0 -0
- flowforge/_classic.html +289 -0
- flowforge/_dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- flowforge/_dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- flowforge/_dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- flowforge/_dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- flowforge/_dashboard/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- flowforge/_dashboard/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- flowforge/_dashboard/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- flowforge/_dashboard/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- flowforge/_dashboard/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- flowforge/_dashboard/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- flowforge/_dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- flowforge/_dashboard/assets/index-BGT5seIN.css +1 -0
- flowforge/_dashboard/assets/index-BoGikST6.js +52 -0
- flowforge/_dashboard/favicon.svg +1 -0
- flowforge/_dashboard/index.html +16 -0
- flowforge/_example/trip-planner/.env.example +3 -0
- flowforge/_example/trip-planner/.gitignore +2 -0
- flowforge/_example/trip-planner/.mcp.json +20 -0
- flowforge/_example/trip-planner/README.md +48 -0
- flowforge/_example/trip-planner/config.py +27 -0
- flowforge/_example/trip-planner/mcp_servers/currency.py +10 -0
- flowforge/_example/trip-planner/planner.py +113 -0
- flowforge/_example/trip-planner/requirements.txt +3 -0
- flowforge/_example/trip-planner/services/__init__.py +0 -0
- flowforge/_example/trip-planner/services/geo.py +49 -0
- flowforge/_example/trip-planner/services/llm.py +18 -0
- flowforge/_example/trip-planner/services/mcp_tools.py +26 -0
- flowforge/_example/trip-planner/services/weather.py +25 -0
- flowforge/_example/trip-planner/tools/packing_list.py +24 -0
- flowforge/_example/trip-planner/trips/.gitkeep +0 -0
- flowforge/_workflows/diamond_mock.json +71 -0
- flowforge/_workflows/stripe_to_razorpay.json +38 -0
- flowforge/agent/__init__.py +5 -0
- flowforge/agent/server.py +333 -0
- flowforge/api/__init__.py +0 -0
- flowforge/api/agent.py +79 -0
- flowforge/api/checks.py +255 -0
- flowforge/api/connectors.py +289 -0
- flowforge/api/flowmap.py +174 -0
- flowforge/api/runs.py +284 -0
- flowforge/api/scan.py +117 -0
- flowforge/api/security.py +31 -0
- flowforge/api/stats.py +80 -0
- flowforge/checks/__init__.py +13 -0
- flowforge/checks/models.py +41 -0
- flowforge/checks/probes.py +359 -0
- flowforge/cli.py +108 -0
- flowforge/connectors/__init__.py +0 -0
- flowforge/connectors/catalog.py +84 -0
- flowforge/connectors/models.py +149 -0
- flowforge/connectors/presets.py +192 -0
- flowforge/connectors/registry.py +85 -0
- flowforge/connectors/runtime.py +47 -0
- flowforge/connectors/secrets.py +53 -0
- flowforge/example_data.py +247 -0
- flowforge/flow/__init__.py +328 -0
- flowforge/flow/advice.py +132 -0
- flowforge/flow/curly.py +884 -0
- flowforge/flow/index.py +116 -0
- flowforge/flow/order.py +181 -0
- flowforge/flow/reach.py +145 -0
- flowforge/flow/rewrite.py +146 -0
- flowforge/flowmap/__init__.py +1 -0
- flowforge/flowmap/diff.py +145 -0
- flowforge/flowmap/fixture.py +283 -0
- flowforge/flowmap/models.py +258 -0
- flowforge/flowmap/planner.py +131 -0
- flowforge/flowmap/service.py +379 -0
- flowforge/flowmap/validate.py +228 -0
- flowforge/home.py +58 -0
- flowforge/main.py +137 -0
- flowforge/nodes/__init__.py +3 -0
- flowforge/nodes/base.py +74 -0
- flowforge/nodes/check_node.py +50 -0
- flowforge/nodes/http_node.py +161 -0
- flowforge/nodes/llm_node.py +158 -0
- flowforge/nodes/local_node.py +164 -0
- flowforge/nodes/mcp_node.py +243 -0
- flowforge/nodes/mock_node.py +40 -0
- flowforge/scan/__init__.py +6 -0
- flowforge/scan/facts.py +47 -0
- flowforge/scan/files.py +112 -0
- flowforge/scan/manifests.py +65 -0
- flowforge/scan/mcp_config.py +75 -0
- flowforge/scan/models.py +73 -0
- flowforge/scan/patterns.py +86 -0
- flowforge/scan/python_ast.py +246 -0
- flowforge/scan/scanner.py +344 -0
- flowforge/scan/services.py +58 -0
- flowforge/scan/urls.py +60 -0
- flowforge/scheduler/__init__.py +0 -0
- flowforge/scheduler/cache.py +76 -0
- flowforge/scheduler/critical_path.py +65 -0
- flowforge/scheduler/durations.py +35 -0
- flowforge/scheduler/executor.py +390 -0
- flowforge/scheduler/graph.py +108 -0
- flowforge/scheduler/rate_limit.py +36 -0
- flowforge/schema.py +81 -0
- flowforge/security/__init__.py +0 -0
- flowforge/security/finders.py +127 -0
- flowforge/security/findings.py +169 -0
- flowforge/security/logfilter.py +129 -0
- flowforge/spa.py +52 -0
- flowforge/state.py +73 -0
- flowforge/storage.py +481 -0
- flowforge/templating.py +60 -0
- flowforge_map-0.1.0.dist-info/METADATA +335 -0
- flowforge_map-0.1.0.dist-info/RECORD +113 -0
- flowforge_map-0.1.0.dist-info/WHEEL +4 -0
- flowforge_map-0.1.0.dist-info/entry_points.txt +3 -0
- flowforge_map-0.1.0.dist-info/licenses/LICENSE +21 -0
flowforge/flow/advice.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""What could run together (D25), from the drawn flow and its weights.
|
|
2
|
+
|
|
3
|
+
Each step starts as early as its lines allow (its earliest start). Steps of one entry point that would start at the
|
|
4
|
+
same moment, but that the code awaits one after another, are one piece of advice: running them together saves the
|
|
5
|
+
time of all but the slowest. Steps the code already gathers, and steps in a loop, get none.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
|
|
13
|
+
from flowforge.flow.rewrite import Call, change, prompt
|
|
14
|
+
from flowforge.flowmap.models import Advice, Flow
|
|
15
|
+
from flowforge.scheduler.graph import DAG, topological_order
|
|
16
|
+
|
|
17
|
+
MIN_SAVING_MS = 50 # below this, advice is noise
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _starts(flow: Flow) -> dict[str, float]:
|
|
21
|
+
parents: dict[str, list[str]] = {n.id: [] for n in flow.nodes}
|
|
22
|
+
children: dict[str, list[str]] = {n.id: [] for n in flow.nodes}
|
|
23
|
+
for e in flow.edges:
|
|
24
|
+
parents[e.target].append(e.source)
|
|
25
|
+
children[e.source].append(e.target)
|
|
26
|
+
weight = {n.id: n.estimate_ms or 0.0 for n in flow.nodes}
|
|
27
|
+
start: dict[str, float] = {}
|
|
28
|
+
for node in topological_order(DAG(nodes=[n.id for n in flow.nodes], parents=parents, children=children)):
|
|
29
|
+
start[node] = max((start[p] + weight[p] for p in parents[node]), default=0.0)
|
|
30
|
+
return start
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def advise(flow: Flow, meta: dict[str, dict], sources: dict[str, str] | None = None,
|
|
34
|
+
modules: dict[str, ast.Module] | None = None) -> list[Advice]:
|
|
35
|
+
start = _starts(flow)
|
|
36
|
+
by_id = {n.id: n for n in flow.nodes}
|
|
37
|
+
groups: dict[tuple[str, float], list[str]] = defaultdict(list)
|
|
38
|
+
for node_id, m in meta.items():
|
|
39
|
+
if not m["gathered"] and not m["loop"]:
|
|
40
|
+
groups[(m["entry"], round(start[node_id], 3))].append(node_id)
|
|
41
|
+
out: list[Advice] = []
|
|
42
|
+
for (_, _), ids in groups.items():
|
|
43
|
+
stmts = {meta[i]["stmt"] for i in ids}
|
|
44
|
+
if len(ids) < 2 or len(stmts) < 2:
|
|
45
|
+
continue
|
|
46
|
+
weights = [by_id[i].estimate_ms or 0.0 for i in ids]
|
|
47
|
+
saved = sum(weights) - max(weights)
|
|
48
|
+
if saved < MIN_SAVING_MS:
|
|
49
|
+
continue
|
|
50
|
+
ids.sort(key=lambda i: meta[i]["line"])
|
|
51
|
+
if all(meta[i].get("awaited", meta[i]["async"]) for i in ids):
|
|
52
|
+
how, hint = "start them together with asyncio.gather", "for example with asyncio.gather"
|
|
53
|
+
elif all(meta[i]["async"] for i in ids): # a plain call inside async code blocks the loop: give it a thread
|
|
54
|
+
how = hint = "start them together with asyncio.gather, running the plain calls in asyncio.to_thread"
|
|
55
|
+
else:
|
|
56
|
+
how, hint = ("they could run at the same time, for example in a thread pool",
|
|
57
|
+
"for example in a thread pool (concurrent.futures)")
|
|
58
|
+
names = [meta[i]["name"] for i in ids]
|
|
59
|
+
text = f"None of these uses another's result: {how}."
|
|
60
|
+
caveat = _caveat(flow, ids, meta, names)
|
|
61
|
+
advice = Advice(nodes=ids, refs=[meta[i]["ref"] for i in ids], text=text, saved_ms=round(saved, 1),
|
|
62
|
+
caveat=caveat)
|
|
63
|
+
lang = meta[ids[0]].get("lang")
|
|
64
|
+
if sources is not None and lang is not None and all(meta[i].get("lang") is lang for i in ids):
|
|
65
|
+
calls = [Call(stmt=meta[i]["node"], func=meta[i]["func"], path=meta[i]["func"].path,
|
|
66
|
+
words=by_id[i].what or by_id[i].title, ref=meta[i]["ref"], weight=by_id[i].estimate_ms or 0.0,
|
|
67
|
+
line=meta[i]["line"]) for i in ids]
|
|
68
|
+
if lang.name == "python":
|
|
69
|
+
func = meta[ids[0]]["func"]
|
|
70
|
+
py = [Call(c.stmt, c.func.node, c.path, c.words, c.ref, c.weight, c.line) for c in calls]
|
|
71
|
+
if modules is not None and all(isinstance(c.stmt, ast.stmt) for c in py):
|
|
72
|
+
advice.change = change(py, sources[func.path], modules[func.path])
|
|
73
|
+
else:
|
|
74
|
+
gather = lang.gather(meta[ids[0]]["func"])
|
|
75
|
+
hint = f"for example with {gather}"
|
|
76
|
+
advice.text = f"None of these uses another's result: start them together with {gather}."
|
|
77
|
+
advice.change = lang.change(calls)
|
|
78
|
+
advice.prompt = prompt(calls, by_id[meta[ids[0]]["entry"]].title, advice.saved_ms, caveat, hint)
|
|
79
|
+
out.append(advice)
|
|
80
|
+
return sorted(out, key=lambda a: -a.saved_ms)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _caveat(flow: Flow, ids: list[str], meta: dict[str, dict], names: list[str]) -> str | None:
|
|
84
|
+
"""Steps that need no other call's result, only an early return's go-ahead: they could start even before that
|
|
85
|
+
check, but when the code stops there, those calls are wasted."""
|
|
86
|
+
into: dict[str, list[str]] = defaultdict(list)
|
|
87
|
+
for e in flow.edges:
|
|
88
|
+
into[e.target].append(e.source)
|
|
89
|
+
by_id = {n.id: n for n in flow.nodes}
|
|
90
|
+
early, guard = [], None
|
|
91
|
+
for i in ids:
|
|
92
|
+
sources = into[i]
|
|
93
|
+
if sources and not meta[i]["ifs"] and not meta[i].get("needs_data") \
|
|
94
|
+
and all(by_id[s].kind == "decision" for s in sources):
|
|
95
|
+
stops = [s for s in sources if any(e.source == s and by_id[e.target].kind == "end" for e in flow.edges)]
|
|
96
|
+
if stops and len(stops) == len(sources):
|
|
97
|
+
early.append(meta[i]["name"])
|
|
98
|
+
guard = by_id[stops[0]].evidence[0].ref
|
|
99
|
+
if not early or guard is None:
|
|
100
|
+
return None
|
|
101
|
+
who = early[0] if len(early) == 1 else f"{', '.join(early[:-1])} and {early[-1]}"
|
|
102
|
+
return (f"{who} only need what was known before the check at {guard}, so they could start even earlier, "
|
|
103
|
+
"but when the code stops there, those calls are wasted.")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def written_ms(flow: Flow, meta: dict[str, dict]) -> float:
|
|
107
|
+
"""The slowest entry point as the code is written. Entry points are separate actions, so they are never added
|
|
108
|
+
together (D32)."""
|
|
109
|
+
weight = {n.id: n.estimate_ms or 0.0 for n in flow.nodes}
|
|
110
|
+
return round(max((written_of(p, weight) for p in parts_by_entry(meta).values()), default=0.0), 1)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def parts_by_entry(meta: dict[str, dict]) -> dict[str, list[list[str]]]:
|
|
114
|
+
"""Each entry point's calls as written: one group per call, or one per statement that already gathers its calls."""
|
|
115
|
+
out: dict[str, list[list[str]]] = defaultdict(list)
|
|
116
|
+
gathered: dict[tuple[str, int], list[str]] = {}
|
|
117
|
+
for node_id, m in meta.items():
|
|
118
|
+
if m["gathered"]:
|
|
119
|
+
key = (m["entry"], m["stmt"])
|
|
120
|
+
if key not in gathered:
|
|
121
|
+
gathered[key] = []
|
|
122
|
+
out[m["entry"]].append(gathered[key])
|
|
123
|
+
gathered[key].append(node_id)
|
|
124
|
+
else:
|
|
125
|
+
out[m["entry"]].append([node_id])
|
|
126
|
+
return dict(out)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def written_of(parts: list[list[str]], weight: dict[str, float]) -> float:
|
|
130
|
+
"""One call after another; a group the code runs together counts once, at its slowest. Calls not in `weight`
|
|
131
|
+
(hidden from the map) count nothing."""
|
|
132
|
+
return sum(max((weight[i] for i in p if i in weight), default=0.0) for p in parts)
|