darabos commited on
Commit
18bc591
·
1 Parent(s): e1a2778

Update the LynxScribe ops with OpenAI API change.

Browse files
lynxkite-app/.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ /src/lynxkite/app/web_assets
2
+ !/src/lynxkite/app/web_assets/__init__.py
3
+ !/src/lynxkite/app/web_assets/assets/__init__.py
lynxkite-app/src/lynxkite/app/main.py CHANGED
@@ -20,7 +20,6 @@ def detect_plugins():
20
  print("No modules found in lynxkite_plugins. Be sure to install some plugins.")
21
  return {}
22
 
23
- print(list(pkgutil.iter_modules(lynxkite_plugins.__path__)))
24
  plugins = {}
25
  for _, name, _ in pkgutil.iter_modules(lynxkite_plugins.__path__):
26
  name = f"lynxkite_plugins.{name}"
 
20
  print("No modules found in lynxkite_plugins. Be sure to install some plugins.")
21
  return {}
22
 
 
23
  plugins = {}
24
  for _, name, _ in pkgutil.iter_modules(lynxkite_plugins.__path__):
25
  name = f"lynxkite_plugins.{name}"
lynxkite-core/build/lib/lynxkite/executors/one_by_one.py DELETED
@@ -1,175 +0,0 @@
1
- """A LynxKite executor that assumes most operations operate on their input one by one."""
2
-
3
- from .. import ops
4
- from .. import workspace
5
- import orjson
6
- import pandas as pd
7
- import pydantic
8
- import traceback
9
- import inspect
10
- import typing
11
-
12
-
13
- class Context(ops.BaseConfig):
14
- """Passed to operation functions as "_ctx" if they have such a parameter."""
15
-
16
- node: workspace.WorkspaceNode
17
- last_result: typing.Any = None
18
-
19
-
20
- class Output(ops.BaseConfig):
21
- """Return this to send values to specific outputs of a node."""
22
-
23
- output_handle: str
24
- value: dict
25
-
26
-
27
- def df_to_list(df):
28
- return df.to_dict(orient="records")
29
-
30
-
31
- def has_ctx(op):
32
- sig = inspect.signature(op.func)
33
- return "_ctx" in sig.parameters
34
-
35
-
36
- CACHES = {}
37
-
38
-
39
- def register(env: str, cache: bool = True):
40
- """Registers the one-by-one executor."""
41
- if cache:
42
- CACHES[env] = {}
43
- cache = CACHES[env]
44
- else:
45
- cache = None
46
- ops.EXECUTORS[env] = lambda ws: execute(ws, ops.CATALOGS[env], cache=cache)
47
-
48
-
49
- def get_stages(ws, catalog):
50
- """Inputs on top/bottom are batch inputs. We decompose the graph into a DAG of components along these edges."""
51
- nodes = {n.id: n for n in ws.nodes}
52
- batch_inputs = {}
53
- inputs = {}
54
- for edge in ws.edges:
55
- inputs.setdefault(edge.target, []).append(edge.source)
56
- node = nodes[edge.target]
57
- op = catalog[node.data.title]
58
- i = op.inputs[edge.targetHandle]
59
- if i.position in "top or bottom":
60
- batch_inputs.setdefault(edge.target, []).append(edge.source)
61
- stages = []
62
- for bt, bss in batch_inputs.items():
63
- upstream = set(bss)
64
- new = set(bss)
65
- while new:
66
- n = new.pop()
67
- for i in inputs.get(n, []):
68
- if i not in upstream:
69
- upstream.add(i)
70
- new.add(i)
71
- stages.append(upstream)
72
- stages.sort(key=lambda s: len(s))
73
- stages.append(set(nodes))
74
- return stages
75
-
76
-
77
- def _default_serializer(obj):
78
- if isinstance(obj, pydantic.BaseModel):
79
- return obj.dict()
80
- return {"__nonserializable__": id(obj)}
81
-
82
-
83
- def make_cache_key(obj):
84
- return orjson.dumps(obj, default=_default_serializer)
85
-
86
-
87
- EXECUTOR_OUTPUT_CACHE = {}
88
-
89
-
90
- async def await_if_needed(obj):
91
- if inspect.isawaitable(obj):
92
- return await obj
93
- return obj
94
-
95
-
96
- async def execute(ws, catalog, cache=None):
97
- nodes = {n.id: n for n in ws.nodes}
98
- contexts = {n.id: Context(node=n) for n in ws.nodes}
99
- edges = {n.id: [] for n in ws.nodes}
100
- for e in ws.edges:
101
- edges[e.source].append(e)
102
- tasks = {}
103
- NO_INPUT = object() # Marker for initial tasks.
104
- for node in ws.nodes:
105
- node.data.error = None
106
- op = catalog.get(node.data.title)
107
- if op is None:
108
- node.data.error = f'Operation "{node.data.title}" not found.'
109
- continue
110
- # Start tasks for nodes that have no non-batch inputs.
111
- if all([i.position in "top or bottom" for i in op.inputs.values()]):
112
- tasks[node.id] = [NO_INPUT]
113
- batch_inputs = {}
114
- # Run the rest until we run out of tasks.
115
- stages = get_stages(ws, catalog)
116
- for stage in stages:
117
- next_stage = {}
118
- while tasks:
119
- n, ts = tasks.popitem()
120
- if n not in stage:
121
- next_stage.setdefault(n, []).extend(ts)
122
- continue
123
- node = nodes[n]
124
- data = node.data
125
- op = catalog[data.title]
126
- params = {**data.params}
127
- if has_ctx(op):
128
- params["_ctx"] = contexts[node.id]
129
- results = []
130
- for task in ts:
131
- try:
132
- inputs = []
133
- for i in op.inputs.values():
134
- if i.position in "top or bottom":
135
- assert (n, i.name) in batch_inputs, f"{i.name} is missing"
136
- inputs.append(batch_inputs[(n, i.name)])
137
- else:
138
- inputs.append(task)
139
- if cache is not None:
140
- key = make_cache_key((inputs, params))
141
- if key not in cache:
142
- cache[key] = await await_if_needed(op(*inputs, **params))
143
- result = cache[key]
144
- else:
145
- result = await await_if_needed(op(*inputs, **params))
146
- except Exception as e:
147
- traceback.print_exc()
148
- data.error = str(e)
149
- break
150
- contexts[node.id].last_result = result
151
- # Returned lists and DataFrames are considered multiple tasks.
152
- if isinstance(result, pd.DataFrame):
153
- result = df_to_list(result)
154
- elif not isinstance(result, list):
155
- result = [result]
156
- results.extend(result)
157
- else: # Finished all tasks without errors.
158
- if (
159
- op.type == "visualization"
160
- or op.type == "table_view"
161
- or op.type == "image"
162
- ):
163
- data.display = results[0]
164
- for edge in edges[node.id]:
165
- t = nodes[edge.target]
166
- op = catalog[t.data.title]
167
- i = op.inputs[edge.targetHandle]
168
- if i.position in "top or bottom":
169
- batch_inputs.setdefault(
170
- (edge.target, edge.targetHandle), []
171
- ).extend(results)
172
- else:
173
- tasks.setdefault(edge.target, []).extend(results)
174
- tasks = next_stage
175
- return contexts
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-core/build/lib/lynxkite/ops.py DELETED
@@ -1,224 +0,0 @@
1
- """API for implementing LynxKite operations."""
2
-
3
- from __future__ import annotations
4
- import enum
5
- import functools
6
- import inspect
7
- import pydantic
8
- import typing
9
- from typing_extensions import Annotated
10
-
11
- CATALOGS = {}
12
- EXECUTORS = {}
13
-
14
- typeof = type # We have some arguments called "type".
15
-
16
-
17
- def type_to_json(t):
18
- if isinstance(t, type) and issubclass(t, enum.Enum):
19
- return {"enum": list(t.__members__.keys())}
20
- if getattr(t, "__metadata__", None):
21
- return t.__metadata__[-1]
22
- return {"type": str(t)}
23
-
24
-
25
- Type = Annotated[typing.Any, pydantic.PlainSerializer(type_to_json, return_type=dict)]
26
- LongStr = Annotated[str, {"format": "textarea"}]
27
- PathStr = Annotated[str, {"format": "path"}]
28
- CollapsedStr = Annotated[str, {"format": "collapsed"}]
29
- NodeAttribute = Annotated[str, {"format": "node attribute"}]
30
- EdgeAttribute = Annotated[str, {"format": "edge attribute"}]
31
-
32
-
33
- class BaseConfig(pydantic.BaseModel):
34
- model_config = pydantic.ConfigDict(
35
- arbitrary_types_allowed=True,
36
- )
37
-
38
-
39
- class Parameter(BaseConfig):
40
- """Defines a parameter for an operation."""
41
-
42
- name: str
43
- default: typing.Any
44
- type: Type = None
45
-
46
- @staticmethod
47
- def options(name, options, default=None):
48
- e = enum.Enum(f"OptionsFor_{name}", options)
49
- return Parameter.basic(name, e[default or options[0]], e)
50
-
51
- @staticmethod
52
- def collapsed(name, default, type=None):
53
- return Parameter.basic(name, default, CollapsedStr)
54
-
55
- @staticmethod
56
- def basic(name, default=None, type=None):
57
- if default is inspect._empty:
58
- default = None
59
- if type is None or type is inspect._empty:
60
- type = typeof(default) if default is not None else None
61
- return Parameter(name=name, default=default, type=type)
62
-
63
-
64
- class Input(BaseConfig):
65
- name: str
66
- type: Type
67
- position: str = "left"
68
-
69
-
70
- class Output(BaseConfig):
71
- name: str
72
- type: Type
73
- position: str = "right"
74
-
75
-
76
- MULTI_INPUT = Input(name="multi", type="*")
77
-
78
-
79
- def basic_inputs(*names):
80
- return {name: Input(name=name, type=None) for name in names}
81
-
82
-
83
- def basic_outputs(*names):
84
- return {name: Output(name=name, type=None) for name in names}
85
-
86
-
87
- class Op(BaseConfig):
88
- func: typing.Callable = pydantic.Field(exclude=True)
89
- name: str
90
- params: dict[str, Parameter]
91
- inputs: dict[str, Input]
92
- outputs: dict[str, Output]
93
- type: str = "basic" # The UI to use for this operation.
94
-
95
- def __call__(self, *inputs, **params):
96
- # Convert parameters.
97
- for p in params:
98
- if p in self.params:
99
- if self.params[p].type == int:
100
- params[p] = int(params[p])
101
- elif self.params[p].type == float:
102
- params[p] = float(params[p])
103
- elif isinstance(self.params[p].type, enum.EnumMeta):
104
- params[p] = self.params[p].type[params[p]]
105
- res = self.func(*inputs, **params)
106
- return res
107
-
108
-
109
- def op(env: str, name: str, *, view="basic", outputs=None):
110
- """Decorator for defining an operation."""
111
-
112
- def decorator(func):
113
- sig = inspect.signature(func)
114
- # Positional arguments are inputs.
115
- inputs = {
116
- name: Input(name=name, type=param.annotation)
117
- for name, param in sig.parameters.items()
118
- if param.kind != param.KEYWORD_ONLY
119
- }
120
- params = {}
121
- for n, param in sig.parameters.items():
122
- if param.kind == param.KEYWORD_ONLY and not n.startswith("_"):
123
- params[n] = Parameter.basic(n, param.default, param.annotation)
124
- if outputs:
125
- _outputs = {name: Output(name=name, type=None) for name in outputs}
126
- else:
127
- _outputs = (
128
- {"output": Output(name="output", type=None)} if view == "basic" else {}
129
- )
130
- op = Op(
131
- func=func,
132
- name=name,
133
- params=params,
134
- inputs=inputs,
135
- outputs=_outputs,
136
- type=view,
137
- )
138
- CATALOGS.setdefault(env, {})
139
- CATALOGS[env][name] = op
140
- func.__op__ = op
141
- return func
142
-
143
- return decorator
144
-
145
-
146
- def input_position(**kwargs):
147
- """Decorator for specifying unusual positions for the inputs."""
148
-
149
- def decorator(func):
150
- op = func.__op__
151
- for k, v in kwargs.items():
152
- op.inputs[k].position = v
153
- return func
154
-
155
- return decorator
156
-
157
-
158
- def output_position(**kwargs):
159
- """Decorator for specifying unusual positions for the outputs."""
160
-
161
- def decorator(func):
162
- op = func.__op__
163
- for k, v in kwargs.items():
164
- op.outputs[k].position = v
165
- return func
166
-
167
- return decorator
168
-
169
-
170
- def no_op(*args, **kwargs):
171
- if args:
172
- return args[0]
173
- return None
174
-
175
-
176
- def register_passive_op(env: str, name: str, inputs=[], outputs=["output"], params=[]):
177
- """A passive operation has no associated code."""
178
- op = Op(
179
- func=no_op,
180
- name=name,
181
- params={p.name: p for p in params},
182
- inputs=dict(
183
- (i, Input(name=i, type=None)) if isinstance(i, str) else (i.name, i)
184
- for i in inputs
185
- ),
186
- outputs=dict(
187
- (o, Output(name=o, type=None)) if isinstance(o, str) else (o.name, o)
188
- for o in outputs
189
- ),
190
- )
191
- CATALOGS.setdefault(env, {})
192
- CATALOGS[env][name] = op
193
- return op
194
-
195
-
196
- def register_executor(env: str):
197
- """Decorator for registering an executor."""
198
-
199
- def decorator(func):
200
- EXECUTORS[env] = func
201
- return func
202
-
203
- return decorator
204
-
205
-
206
- def op_registration(env: str):
207
- return functools.partial(op, env)
208
-
209
-
210
- def passive_op_registration(env: str):
211
- return functools.partial(register_passive_op, env)
212
-
213
-
214
- def register_area(env, name, params=[]):
215
- """A node that represents an area. It can contain other nodes, but does not restrict movement in any way."""
216
- op = Op(
217
- func=no_op,
218
- name=name,
219
- params={p.name: p for p in params},
220
- inputs={},
221
- outputs={},
222
- type="area",
223
- )
224
- CATALOGS[env][name] = op
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-core/build/lib/lynxkite/workspace.py DELETED
@@ -1,96 +0,0 @@
1
- """For working with LynxKite workspaces."""
2
-
3
- from typing import Optional
4
- import dataclasses
5
- import os
6
- import pydantic
7
- import tempfile
8
- from . import ops
9
-
10
-
11
- class BaseConfig(pydantic.BaseModel):
12
- model_config = pydantic.ConfigDict(
13
- extra="allow",
14
- )
15
-
16
-
17
- class Position(BaseConfig):
18
- x: float
19
- y: float
20
-
21
-
22
- class WorkspaceNodeData(BaseConfig):
23
- title: str
24
- params: dict
25
- display: Optional[object] = None
26
- error: Optional[str] = None
27
- # Also contains a "meta" field when going out.
28
- # This is ignored when coming back from the frontend.
29
-
30
-
31
- class WorkspaceNode(BaseConfig):
32
- id: str
33
- type: str
34
- data: WorkspaceNodeData
35
- position: Position
36
-
37
-
38
- class WorkspaceEdge(BaseConfig):
39
- id: str
40
- source: str
41
- target: str
42
- sourceHandle: str
43
- targetHandle: str
44
-
45
-
46
- class Workspace(BaseConfig):
47
- env: str = ""
48
- nodes: list[WorkspaceNode] = dataclasses.field(default_factory=list)
49
- edges: list[WorkspaceEdge] = dataclasses.field(default_factory=list)
50
-
51
-
52
- async def execute(ws: Workspace):
53
- if ws.env in ops.EXECUTORS:
54
- await ops.EXECUTORS[ws.env](ws)
55
-
56
-
57
- def save(ws: Workspace, path: str):
58
- j = ws.model_dump_json(indent=2)
59
- dirname, basename = os.path.split(path)
60
- # Create temp file in the same directory to make sure it's on the same filesystem.
61
- with tempfile.NamedTemporaryFile(
62
- "w", prefix=f".{basename}.", dir=dirname, delete=False
63
- ) as f:
64
- temp_name = f.name
65
- f.write(j)
66
- os.replace(temp_name, path)
67
-
68
-
69
- def load(path: str):
70
- with open(path) as f:
71
- j = f.read()
72
- ws = Workspace.model_validate_json(j)
73
- # Metadata is added after loading. This way code changes take effect on old boxes too.
74
- _update_metadata(ws)
75
- return ws
76
-
77
-
78
- def _update_metadata(ws):
79
- catalog = ops.CATALOGS.get(ws.env, {})
80
- nodes = {node.id: node for node in ws.nodes}
81
- done = set()
82
- while len(done) < len(nodes):
83
- for node in ws.nodes:
84
- if node.id in done:
85
- continue
86
- data = node.data
87
- op = catalog.get(data.title)
88
- if op:
89
- data.meta = op
90
- node.type = op.type
91
- if data.error == "Unknown operation.":
92
- data.error = None
93
- else:
94
- data.error = "Unknown operation."
95
- done.add(node.id)
96
- return ws