darabos commited on
Commit
a07e9cb
·
1 Parent(s): e7fa7ee

Start writing "LLM logic" executor.

Browse files
server/llm_ops.py CHANGED
@@ -1,13 +1,29 @@
1
  '''For specifying an LLM agent logic flow.'''
2
  from . import ops
 
 
3
  import json
4
  import openai
5
  import pandas as pd
 
 
6
 
7
  client = openai.OpenAI(base_url="http://localhost:11434/v1")
8
  CACHE = {}
 
 
9
 
10
- op = ops.op_registration('LLM logic')
 
 
 
 
 
 
 
 
 
 
11
 
12
  def chat(*args, **kwargs):
13
  key = json.dumps({'args': args, 'kwargs': kwargs})
@@ -23,52 +39,129 @@ def input(*, filename: ops.PathStr, key: str):
23
  @op("Create prompt")
24
  def create_prompt(input, *, template: ops.LongStr):
25
  assert template, 'Please specify the template. Refer to columns using their names in uppercase.'
26
- df = input.copy()
27
- prompts = []
28
- for i, row in df.iterrows():
29
- p = template
30
- for c in df.columns:
31
- p = p.replace(c.upper(), str(row[c]))
32
- prompts.append(p)
33
- df['prompt'] = prompts
34
- return df
35
-
36
 
37
  @op("Ask LLM")
38
  def ask_llm(input, *, model: str, accepted_regex: str = None, max_tokens: int = 100):
39
  assert model, 'Please specify the model.'
40
- assert 'prompt' in input.columns, 'Please create the prompt first.'
41
- df = input.copy()
42
- g = {}
43
  if accepted_regex:
44
- g['extra_body'] = {
45
  "guided_regex": accepted_regex,
46
  }
47
- for i, row in df.iterrows():
48
- [res] = chat(
49
- model=model,
50
- max_tokens=max_tokens,
51
- messages=[
52
- {"role": "user", "content": row['prompt']},
53
- ],
54
- **g,
55
- )
56
- df.loc[i, 'response'] = res
57
- return df
58
 
59
  @op("View", view="table_view")
60
- def view(input):
61
- v = {
62
- 'dataframes': { 'df': {
63
- 'columns': [str(c) for c in input.columns],
64
- 'data': input.values.tolist(),
65
- }}
66
- }
 
 
 
 
 
 
67
  return v
68
 
69
  @ops.input_position(input="right")
70
  @ops.output_position(output="left")
71
  @op("Loop")
72
- def loop(input, *, max_iterations: int = 10):
73
- '''Data can flow back here until it becomes empty or reaches the limit.'''
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  return input
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  '''For specifying an LLM agent logic flow.'''
2
  from . import ops
3
+ import dataclasses
4
+ import inspect
5
  import json
6
  import openai
7
  import pandas as pd
8
+ import traceback
9
+ from . import workspace
10
 
11
  client = openai.OpenAI(base_url="http://localhost:11434/v1")
12
  CACHE = {}
13
+ ENV = 'LLM logic'
14
+ op = ops.op_registration(ENV)
15
 
16
+ @dataclasses.dataclass
17
+ class Context:
18
+ '''Passed to operation functions as "_ctx" if they have such a parameter.'''
19
+ node: workspace.WorkspaceNode
20
+ last_result = None
21
+
22
+ @dataclasses.dataclass
23
+ class Output:
24
+ '''Return this to send values to specific outputs of a node.'''
25
+ output_handle: str
26
+ value: dict
27
 
28
  def chat(*args, **kwargs):
29
  key = json.dumps({'args': args, 'kwargs': kwargs})
 
39
  @op("Create prompt")
40
  def create_prompt(input, *, template: ops.LongStr):
41
  assert template, 'Please specify the template. Refer to columns using their names in uppercase.'
42
+ p = template
43
+ for k, v in input.items():
44
+ p = p.replace(k.upper(), str(v))
45
+ return p
 
 
 
 
 
 
46
 
47
  @op("Ask LLM")
48
  def ask_llm(input, *, model: str, accepted_regex: str = None, max_tokens: int = 100):
49
  assert model, 'Please specify the model.'
50
+ assert 'prompt' in input, 'Please create the prompt first.'
51
+ options = {}
 
52
  if accepted_regex:
53
+ options['extra_body'] = {
54
  "guided_regex": accepted_regex,
55
  }
56
+ results = chat(
57
+ model=model,
58
+ max_tokens=max_tokens,
59
+ messages=[
60
+ {"role": "user", "content": input['prompt']},
61
+ ],
62
+ **options,
63
+ )
64
+ return [{**input, 'response': r} for r in results]
 
 
65
 
66
  @op("View", view="table_view")
67
+ def view(input, *, _ctx: Context):
68
+ v = _ctx.last_result
69
+ if v:
70
+ columns = v['dataframes']['df']['columns']
71
+ v['dataframes']['df']['data'].append([input[c] for c in columns])
72
+ else:
73
+ columns = [str(c) for c in input.keys() if not str(c).startswith('_')]
74
+ v = {
75
+ 'dataframes': { 'df': {
76
+ 'columns': columns,
77
+ 'data': [input[c] for c in columns],
78
+ }}
79
+ }
80
  return v
81
 
82
  @ops.input_position(input="right")
83
  @ops.output_position(output="left")
84
  @op("Loop")
85
+ def loop(input, *, max_iterations: int = 3, _ctx: Context):
86
+ '''Data can flow back here max_iterations-1 times.'''
87
+ key = f'iterations-{_ctx.node.id}'
88
+ input[key] = input.get(key, 0) + 1
89
+ if input[key] < max_iterations:
90
+ return input
91
+
92
+ @op('Branch', outputs=['true', 'false'])
93
+ def branch(input, *, expression: str):
94
+ res = eval(expression, input)
95
+ return Output(str(bool(res)).lower(), input)
96
+
97
+ @ops.input_position(db="top")
98
+ @op('RAG')
99
+ def rag(input, db, *, closest_n: int=10):
100
  return input
101
+
102
+ @op('Run Python')
103
+ def run_python(input, *, template: str):
104
+ assert template, 'Please specify the template. Refer to columns using their names in uppercase.'
105
+ p = template
106
+ for k, v in input.items():
107
+ p = p.replace(k.upper(), str(v))
108
+ return p
109
+
110
+
111
+
112
+ @ops.register_executor(ENV)
113
+ def execute(ws):
114
+ catalog = ops.CATALOGS[ENV]
115
+ nodes = {n.id: n for n in ws.nodes}
116
+ contexts = {n.id: Context(n) for n in ws.nodes}
117
+ edges = {n.id: [] for n in ws.nodes}
118
+ for e in ws.edges:
119
+ edges[e.source].append(e.target)
120
+ tasks = {}
121
+ NO_INPUT = object() # Marker for initial tasks.
122
+ for node in ws.nodes:
123
+ node.data.error = None
124
+ op = catalog[node.data.title]
125
+ # Start tasks for nodes that have no inputs.
126
+ if not op.inputs:
127
+ tasks[node.id] = [NO_INPUT]
128
+ # Run the rest until we run out of tasks.
129
+ while tasks:
130
+ n, ts = tasks.popitem()
131
+ node = nodes[n]
132
+ data = node.data
133
+ op = catalog[data.title]
134
+ params = {**data.params}
135
+ if has_ctx(op):
136
+ params['_ctx'] = contexts[node.id]
137
+ results = []
138
+ for task in ts:
139
+ try:
140
+ if task is NO_INPUT:
141
+ result = op(**params)
142
+ else:
143
+ # TODO: Tasks with multiple inputs?
144
+ result = op(task, **params)
145
+ except Exception as e:
146
+ traceback.print_exc()
147
+ data.error = str(e)
148
+ break
149
+ contexts[node.id].last_result = result
150
+ # Returned lists and DataFrames are considered multiple tasks.
151
+ if isinstance(result, pd.DataFrame):
152
+ result = df_to_list(result)
153
+ elif not isinstance(result, list):
154
+ result = [result]
155
+ results.extend(result)
156
+ else: # Finished all tasks without errors.
157
+ if op.type == 'visualization' or op.type == 'table_view':
158
+ data.display = results
159
+ for target in edges[node.id]:
160
+ tasks.setdefault(target, []).extend(results)
161
+
162
+ def df_to_list(df):
163
+ return [dict(zip(df.columns, row)) for row in df.values]
164
+
165
+ def has_ctx(op):
166
+ sig = inspect.signature(op.func)
167
+ return '_ctx' in sig.parameters
server/lynxkite_ops.py CHANGED
@@ -1,12 +1,135 @@
1
  '''Some operations. To be split into separate files when we have more.'''
2
  from . import ops
 
 
3
  import matplotlib
4
  import networkx as nx
5
  import pandas as pd
6
  import traceback
 
7
 
8
  op = ops.op_registration('LynxKite')
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  @op("Import Parquet")
11
  def import_parquet(*, filename: str):
12
  '''Imports a parquet file.'''
@@ -18,7 +141,7 @@ def create_scale_free_graph(*, nodes: int = 10):
18
  return nx.scale_free_graph(nodes)
19
 
20
  @op("Compute PageRank")
21
- @ops.nx_node_attribute_func('pagerank')
22
  def compute_pagerank(graph: nx.Graph, *, damping=0.85, iterations=100):
23
  return nx.pagerank(graph, alpha=damping, max_iter=iterations)
24
 
@@ -26,7 +149,7 @@ def compute_pagerank(graph: nx.Graph, *, damping=0.85, iterations=100):
26
  @op("Sample graph")
27
  def sample_graph(graph: nx.Graph, *, nodes: int = 100):
28
  '''Takes a subgraph.'''
29
- return nx.scale_free_graph(nodes)
30
 
31
 
32
  def _map_color(value):
@@ -36,7 +159,7 @@ def _map_color(value):
36
  return ['#{:02x}{:02x}{:02x}'.format(int(r*255), int(g*255), int(b*255)) for r, g, b in rgba[:, :3]]
37
 
38
  @op("Visualize graph", view="visualization")
39
- def visualize_graph(graph: ops.Bundle, *, color_nodes_by: 'node_attribute' = None):
40
  nodes = graph.dfs['nodes'].copy()
41
  if color_nodes_by:
42
  nodes['color'] = _map_color(nodes[color_nodes_by])
@@ -79,7 +202,7 @@ def visualize_graph(graph: ops.Bundle, *, color_nodes_by: 'node_attribute' = Non
79
  return v
80
 
81
  @op("View tables", view="table_view")
82
- def view_tables(bundle: ops.Bundle):
83
  v = {
84
  'dataframes': { name: {
85
  'columns': [str(c) for c in df.columns],
@@ -89,44 +212,3 @@ def view_tables(bundle: ops.Bundle):
89
  'other': bundle.other,
90
  }
91
  return v
92
-
93
- @ops.register_executor('LynxKite')
94
- def execute(ws):
95
- catalog = ops.CATALOGS['LynxKite']
96
- # Nodes are responsible for interpreting/executing their child nodes.
97
- nodes = [n for n in ws.nodes if not n.parentId]
98
- children = {}
99
- for n in ws.nodes:
100
- if n.parentId:
101
- children.setdefault(n.parentId, []).append(n)
102
- outputs = {}
103
- failed = 0
104
- while len(outputs) + failed < len(nodes):
105
- for node in nodes:
106
- if node.id in outputs:
107
- continue
108
- inputs = [edge.source for edge in ws.edges if edge.target == node.id]
109
- if all(input in outputs for input in inputs):
110
- inputs = [outputs[input] for input in inputs]
111
- data = node.data
112
- op = catalog[data.title]
113
- params = {**data.params}
114
- if op.sub_nodes:
115
- sub_nodes = children.get(node.id, [])
116
- sub_node_ids = [node.id for node in sub_nodes]
117
- sub_edges = [edge for edge in ws.edges if edge.source in sub_node_ids]
118
- params['sub_flow'] = {'nodes': sub_nodes, 'edges': sub_edges}
119
- try:
120
- output = op(*inputs, **params)
121
- except Exception as e:
122
- traceback.print_exc()
123
- data.error = str(e)
124
- failed += 1
125
- continue
126
- if len(op.inputs) == 1 and op.inputs.get('multi') == '*':
127
- # It's a flexible input. Create n+1 handles.
128
- data.inputs = {f'input{i}': None for i in range(len(inputs) + 1)}
129
- data.error = None
130
- outputs[node.id] = output
131
- if op.type == 'visualization' or op.type == 'table_view':
132
- data.view = output
 
1
  '''Some operations. To be split into separate files when we have more.'''
2
  from . import ops
3
+ import dataclasses
4
+ import functools
5
  import matplotlib
6
  import networkx as nx
7
  import pandas as pd
8
  import traceback
9
+ import typing
10
 
11
  op = ops.op_registration('LynxKite')
12
 
13
+ @dataclasses.dataclass
14
+ class RelationDefinition:
15
+ '''Defines a set of edges.'''
16
+ df: str # The DataFrame that contains the edges.
17
+ source_column: str # The column in the edge DataFrame that contains the source node ID.
18
+ target_column: str # The column in the edge DataFrame that contains the target node ID.
19
+ source_table: str # The DataFrame that contains the source nodes.
20
+ target_table: str # The DataFrame that contains the target nodes.
21
+ source_key: str # The column in the source table that contains the node ID.
22
+ target_key: str # The column in the target table that contains the node ID.
23
+
24
+ @dataclasses.dataclass
25
+ class Bundle:
26
+ '''A collection of DataFrames and other data.
27
+
28
+ Can efficiently represent a knowledge graph (homogeneous or heterogeneous) or tabular data.
29
+ It can also carry other data, such as a trained model.
30
+ '''
31
+ dfs: dict[str, pd.DataFrame] = dataclasses.field(default_factory=dict)
32
+ relations: list[RelationDefinition] = dataclasses.field(default_factory=list)
33
+ other: dict[str, typing.Any] = None
34
+
35
+ @classmethod
36
+ def from_nx(cls, graph: nx.Graph):
37
+ edges = nx.to_pandas_edgelist(graph)
38
+ d = dict(graph.nodes(data=True))
39
+ nodes = pd.DataFrame(d.values(), index=d.keys())
40
+ nodes['id'] = nodes.index
41
+ return cls(
42
+ dfs={'edges': edges, 'nodes': nodes},
43
+ relations=[
44
+ RelationDefinition(
45
+ df='edges',
46
+ source_column='source',
47
+ target_column='target',
48
+ source_table='nodes',
49
+ target_table='nodes',
50
+ source_key='id',
51
+ target_key='id',
52
+ )
53
+ ]
54
+ )
55
+
56
+ def to_nx(self):
57
+ graph = nx.from_pandas_edgelist(self.dfs['edges'])
58
+ nx.set_node_attributes(graph, self.dfs['nodes'].set_index('id').to_dict('index'))
59
+ return graph
60
+
61
+
62
+ def nx_node_attribute_func(name):
63
+ '''Decorator for wrapping a function that adds a NetworkX node attribute.'''
64
+ def decorator(func):
65
+ @functools.wraps(func)
66
+ def wrapper(graph: nx.Graph, **kwargs):
67
+ graph = graph.copy()
68
+ attr = func(graph, **kwargs)
69
+ nx.set_node_attributes(graph, attr, name)
70
+ return graph
71
+ return wrapper
72
+ return decorator
73
+
74
+
75
+ def disambiguate_edges(ws):
76
+ '''If an input plug is connected to multiple edges, keep only the last edge.'''
77
+ seen = set()
78
+ for edge in reversed(ws.edges):
79
+ if (edge.target, edge.targetHandle) in seen:
80
+ ws.edges.remove(edge)
81
+ seen.add((edge.target, edge.targetHandle))
82
+
83
+
84
+ @ops.register_executor('LynxKite')
85
+ def execute(ws):
86
+ catalog = ops.CATALOGS['LynxKite']
87
+ # Nodes are responsible for interpreting/executing their child nodes.
88
+ nodes = [n for n in ws.nodes if not n.parentId]
89
+ disambiguate_edges(ws)
90
+ children = {}
91
+ for n in ws.nodes:
92
+ if n.parentId:
93
+ children.setdefault(n.parentId, []).append(n)
94
+ outputs = {}
95
+ failed = 0
96
+ while len(outputs) + failed < len(nodes):
97
+ for node in nodes:
98
+ if node.id in outputs:
99
+ continue
100
+ # TODO: Take the input/output handles into account.
101
+ inputs = [edge.source for edge in ws.edges if edge.target == node.id]
102
+ if all(input in outputs for input in inputs):
103
+ inputs = [outputs[input] for input in inputs]
104
+ data = node.data
105
+ op = catalog[data.title]
106
+ params = {**data.params}
107
+ if op.sub_nodes:
108
+ sub_nodes = children.get(node.id, [])
109
+ sub_node_ids = [node.id for node in sub_nodes]
110
+ sub_edges = [edge for edge in ws.edges if edge.source in sub_node_ids]
111
+ params['sub_flow'] = {'nodes': sub_nodes, 'edges': sub_edges}
112
+ # Convert inputs.
113
+ for i, (x, p) in enumerate(zip(inputs, op.inputs.values())):
114
+ if p.type == nx.Graph and isinstance(x, Bundle):
115
+ inputs[i] = x.to_nx()
116
+ elif p.type == Bundle and isinstance(x, nx.Graph):
117
+ inputs[i] = Bundle.from_nx(x)
118
+ try:
119
+ output = op(*inputs, **params)
120
+ except Exception as e:
121
+ traceback.print_exc()
122
+ data.error = str(e)
123
+ failed += 1
124
+ continue
125
+ if len(op.inputs) == 1 and op.inputs.get('multi') == '*':
126
+ # It's a flexible input. Create n+1 handles.
127
+ data.inputs = {f'input{i}': None for i in range(len(inputs) + 1)}
128
+ data.error = None
129
+ outputs[node.id] = output
130
+ if op.type == 'visualization' or op.type == 'table_view':
131
+ data.display = output
132
+
133
  @op("Import Parquet")
134
  def import_parquet(*, filename: str):
135
  '''Imports a parquet file.'''
 
141
  return nx.scale_free_graph(nodes)
142
 
143
  @op("Compute PageRank")
144
+ @nx_node_attribute_func('pagerank')
145
  def compute_pagerank(graph: nx.Graph, *, damping=0.85, iterations=100):
146
  return nx.pagerank(graph, alpha=damping, max_iter=iterations)
147
 
 
149
  @op("Sample graph")
150
  def sample_graph(graph: nx.Graph, *, nodes: int = 100):
151
  '''Takes a subgraph.'''
152
+ return nx.scale_free_graph(nodes) # TODO: Implement this.
153
 
154
 
155
  def _map_color(value):
 
159
  return ['#{:02x}{:02x}{:02x}'.format(int(r*255), int(g*255), int(b*255)) for r, g, b in rgba[:, :3]]
160
 
161
  @op("Visualize graph", view="visualization")
162
+ def visualize_graph(graph: Bundle, *, color_nodes_by: 'node_attribute' = None):
163
  nodes = graph.dfs['nodes'].copy()
164
  if color_nodes_by:
165
  nodes['color'] = _map_color(nodes[color_nodes_by])
 
202
  return v
203
 
204
  @op("View tables", view="table_view")
205
+ def view_tables(bundle: Bundle):
206
  v = {
207
  'dataframes': { name: {
208
  'columns': [str(c) for c in df.columns],
 
212
  'other': bundle.other,
213
  }
214
  return v
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
server/ops.py CHANGED
@@ -95,80 +95,12 @@ class Op(BaseConfig):
95
  params[p] = int(params[p])
96
  elif self.params[p].type == float:
97
  params[p] = float(params[p])
98
- # Convert inputs.
99
- inputs = list(inputs)
100
- for i, (x, p) in enumerate(zip(inputs, self.inputs.values())):
101
- if p.type == nx.Graph and isinstance(x, Bundle):
102
- inputs[i] = x.to_nx()
103
- elif p.type == Bundle and isinstance(x, nx.Graph):
104
- inputs[i] = Bundle.from_nx(x)
105
  res = self.func(*inputs, **params)
106
  return res
107
 
108
 
109
- @dataclasses.dataclass
110
- class RelationDefinition:
111
- '''Defines a set of edges.'''
112
- df: str # The DataFrame that contains the edges.
113
- source_column: str # The column in the edge DataFrame that contains the source node ID.
114
- target_column: str # The column in the edge DataFrame that contains the target node ID.
115
- source_table: str # The DataFrame that contains the source nodes.
116
- target_table: str # The DataFrame that contains the target nodes.
117
- source_key: str # The column in the source table that contains the node ID.
118
- target_key: str # The column in the target table that contains the node ID.
119
-
120
- @dataclasses.dataclass
121
- class Bundle:
122
- '''A collection of DataFrames and other data.
123
-
124
- Can efficiently represent a knowledge graph (homogeneous or heterogeneous) or tabular data.
125
- It can also carry other data, such as a trained model.
126
- '''
127
- dfs: dict[str, pd.DataFrame] = dataclasses.field(default_factory=dict)
128
- relations: list[RelationDefinition] = dataclasses.field(default_factory=list)
129
- other: dict[str, typing.Any] = None
130
-
131
- @classmethod
132
- def from_nx(cls, graph: nx.Graph):
133
- edges = nx.to_pandas_edgelist(graph)
134
- d = dict(graph.nodes(data=True))
135
- nodes = pd.DataFrame(d.values(), index=d.keys())
136
- nodes['id'] = nodes.index
137
- return cls(
138
- dfs={'edges': edges, 'nodes': nodes},
139
- relations=[
140
- RelationDefinition(
141
- df='edges',
142
- source_column='source',
143
- target_column='target',
144
- source_table='nodes',
145
- target_table='nodes',
146
- source_key='id',
147
- target_key='id',
148
- )
149
- ]
150
- )
151
-
152
- def to_nx(self):
153
- graph = nx.from_pandas_edgelist(self.dfs['edges'])
154
- nx.set_node_attributes(graph, self.dfs['nodes'].set_index('id').to_dict('index'))
155
- return graph
156
-
157
-
158
- def nx_node_attribute_func(name):
159
- '''Decorator for wrapping a function that adds a NetworkX node attribute.'''
160
- def decorator(func):
161
- @functools.wraps(func)
162
- def wrapper(graph: nx.Graph, **kwargs):
163
- graph = graph.copy()
164
- attr = func(graph, **kwargs)
165
- nx.set_node_attributes(graph, attr, name)
166
- return graph
167
- return wrapper
168
- return decorator
169
-
170
 
171
- def op(env: str, name: str, *, view='basic', sub_nodes=None):
172
  '''Decorator for defining an operation.'''
173
  def decorator(func):
174
  sig = inspect.signature(func)
@@ -179,10 +111,13 @@ def op(env: str, name: str, *, view='basic', sub_nodes=None):
179
  if param.kind != param.KEYWORD_ONLY}
180
  params = {}
181
  for n, param in sig.parameters.items():
182
- if param.kind == param.KEYWORD_ONLY:
183
  params[n] = Parameter.basic(n, param.default, param.annotation)
184
- outputs = {'output': Output(name='output', type=None)} if view == 'basic' else {} # Maybe more fancy later.
185
- op = Op(func=func, name=name, params=params, inputs=inputs, outputs=outputs, type=view)
 
 
 
186
  if sub_nodes is not None:
187
  op.sub_nodes = sub_nodes
188
  op.type = 'sub_flow'
@@ -213,7 +148,7 @@ def output_position(**kwargs):
213
  def no_op(*args, **kwargs):
214
  if args:
215
  return args[0]
216
- return Bundle()
217
 
218
  def register_passive_op(env: str, name: str, inputs=[], outputs=['output'], params=[]):
219
  '''A passive operation has no associated code.'''
 
95
  params[p] = int(params[p])
96
  elif self.params[p].type == float:
97
  params[p] = float(params[p])
 
 
 
 
 
 
 
98
  res = self.func(*inputs, **params)
99
  return res
100
 
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
+ def op(env: str, name: str, *, view='basic', sub_nodes=None, outputs=None):
104
  '''Decorator for defining an operation.'''
105
  def decorator(func):
106
  sig = inspect.signature(func)
 
111
  if param.kind != param.KEYWORD_ONLY}
112
  params = {}
113
  for n, param in sig.parameters.items():
114
+ if param.kind == param.KEYWORD_ONLY and not n.startswith('_'):
115
  params[n] = Parameter.basic(n, param.default, param.annotation)
116
+ if outputs:
117
+ _outputs = {name: Output(name=name, type=None) for name in outputs}
118
+ else:
119
+ _outputs = {'output': Output(name='output', type=None)} if view == 'basic' else {}
120
+ op = Op(func=func, name=name, params=params, inputs=inputs, outputs=_outputs, type=view)
121
  if sub_nodes is not None:
122
  op.sub_nodes = sub_nodes
123
  op.type = 'sub_flow'
 
148
  def no_op(*args, **kwargs):
149
  if args:
150
  return args[0]
151
+ return None
152
 
153
  def register_passive_op(env: str, name: str, inputs=[], outputs=['output'], params=[]):
154
  '''A passive operation has no associated code.'''
server/test_llm_ops.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+ from . import llm_ops
3
+ from . import workspace
4
+
5
+ class LLMOpsTest(unittest.TestCase):
6
+ def testExecute(self):
7
+ ws = workspace.Workspace(env='LLM logic', nodes=[
8
+ workspace.WorkspaceNode(
9
+ id='0',
10
+ type='basic',
11
+ position=workspace.Position(x=0, y=0),
12
+ data=workspace.WorkspaceNodeData(title='Input', params={
13
+ 'filename': '/Users/danieldarabos/Downloads/aimo-train.csv',
14
+ 'key': 'problem',
15
+ })),
16
+ workspace.WorkspaceNode(
17
+ id='1',
18
+ type='table_view',
19
+ position=workspace.Position(x=0, y=0),
20
+ data=workspace.WorkspaceNodeData(title='View', params={})),
21
+ ], edges=[
22
+ workspace.WorkspaceEdge(id='0-1', source='0', target='1', sourceHandle='', targetHandle=''),
23
+ ])
24
+ llm_ops.execute(ws)
25
+ self.assertEqual('', ws.nodes[1].data.display)
26
+
27
+ if __name__ == '__main__':
28
+ unittest.main()
server/workspace.py CHANGED
@@ -35,6 +35,8 @@ class WorkspaceEdge(BaseConfig):
35
  id: str
36
  source: str
37
  target: str
 
 
38
 
39
  class Workspace(BaseConfig):
40
  env: str = ''
 
35
  id: str
36
  source: str
37
  target: str
38
+ sourceHandle: str
39
+ targetHandle: str
40
 
41
  class Workspace(BaseConfig):
42
  env: str = ''
web/src/Directory.svelte CHANGED
@@ -58,7 +58,9 @@
58
  </div>
59
  <div class="entry-list">
60
  {#await list}
61
- <div>Loading...</div>
 
 
62
  {:then list}
63
  <div class="actions">
64
  <a href="{newWorkspaceIn(path, list)}"><FilePlus /> New workspace</a>
@@ -168,4 +170,8 @@
168
  color: black;
169
  text-decoration: none;
170
  }
 
 
 
 
171
  </style>
 
58
  </div>
59
  <div class="entry-list">
60
  {#await list}
61
+ <div class="loading spinner-border" role="status">
62
+ <span class="visually-hidden">Loading...</span>
63
+ </div>
64
  {:then list}
65
  <div class="actions">
66
  <a href="{newWorkspaceIn(path, list)}"><FilePlus /> New workspace</a>
 
170
  color: black;
171
  text-decoration: none;
172
  }
173
+ .loading {
174
+ color: #39bcf3;
175
+ margin: 10px;
176
+ }
177
  </style>
web/src/EnvironmentSelector.svelte ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let options;
3
+ export let value;
4
+ export let onChange;
5
+ </script>
6
+
7
+ <select class="form-select form-select-sm"
8
+ value={value}
9
+ on:change={(evt) => onChange(evt.currentTarget.value)}
10
+ >
11
+ {#each options as option}
12
+ <option value={option}>{option}</option>
13
+ {/each}
14
+ </select>
web/src/LynxKiteFlow.svelte CHANGED
@@ -160,15 +160,6 @@
160
  console.log('changed', JSON.stringify(diff(g, $backendWorkspace.data), null, 2));
161
  $mutation.mutate({ path, ws: g });
162
  }
163
- function onconnect(connection: Connection) {
164
- edges.update((edges) => {
165
- // Only one source can connect to a given target.
166
- return edges.filter((e) =>
167
- e.source === connection.source
168
- || e.target !== connection.target
169
- || e.targetHandle !== connection.targetHandle);
170
- });
171
- }
172
  function nodeClick(e) {
173
  const node = e.detail.node;
174
  const meta = node.data.meta;
@@ -211,7 +202,6 @@
211
  proOptions={{ hideAttribution: true }}
212
  maxZoom={3}
213
  minZoom={0.3}
214
- onconnect={onconnect}
215
  defaultEdgeOptions={{ markerEnd: { type: MarkerType.Arrow } }}
216
  >
217
  <Controls />
 
160
  console.log('changed', JSON.stringify(diff(g, $backendWorkspace.data), null, 2));
161
  $mutation.mutate({ path, ws: g });
162
  }
 
 
 
 
 
 
 
 
 
163
  function nodeClick(e) {
164
  const node = e.detail.node;
165
  const meta = node.data.meta;
 
202
  proOptions={{ hideAttribution: true }}
203
  maxZoom={3}
204
  minZoom={0.3}
 
205
  defaultEdgeOptions={{ markerEnd: { type: MarkerType.Arrow } }}
206
  >
207
  <Controls />
web/src/NodeWithTableView.svelte CHANGED
@@ -6,18 +6,18 @@
6
  type $$Props = NodeProps;
7
  export let data: $$Props['data'];
8
  const open = {};
9
- $: single = data.view?.dataframes && Object.keys(data.view.dataframes).length === 1;
10
  </script>
11
 
12
  <LynxKiteNode {...$$props}>
13
- {#if data.view}
14
- {#each Object.entries(data.view.dataframes) as [name, df]}
15
  {#if !single}<div class="df-head" on:click={() => open[name] = !open[name]}>{name}</div>{/if}
16
  {#if single || open[name]}
17
  <Table columns={df.columns} data={df.data} />
18
  {/if}
19
  {/each}
20
- {#each Object.entries(data.view.others || {}) as [name, o]}
21
  <div class="df-head" on:click={() => open[name] = !open[name]}>{name}</div>
22
  {#if open[name]}
23
  <pre>{o}</pre>
 
6
  type $$Props = NodeProps;
7
  export let data: $$Props['data'];
8
  const open = {};
9
+ $: single = data.display?.dataframes && Object.keys(data.display.dataframes).length === 1;
10
  </script>
11
 
12
  <LynxKiteNode {...$$props}>
13
+ {#if data.display}
14
+ {#each Object.entries(data.display.dataframes) as [name, df]}
15
  {#if !single}<div class="df-head" on:click={() => open[name] = !open[name]}>{name}</div>{/if}
16
  {#if single || open[name]}
17
  <Table columns={df.columns} data={df.data} />
18
  {/if}
19
  {/each}
20
+ {#each Object.entries(data.display.others || {}) as [name, o]}
21
  <div class="df-head" on:click={() => open[name] = !open[name]}>{name}</div>
22
  {#if open[name]}
23
  <pre>{o}</pre>
web/src/NodeWithVisualization.svelte CHANGED
@@ -8,8 +8,8 @@
8
  </script>
9
 
10
  <NodeWithParams {...$$props}>
11
- {#if data.view}
12
- <Chart {init} options={data.view} initOptions={{renderer: 'canvas', width: 250, height: 250}}/>
13
  {/if}
14
  </NodeWithParams>
15
  <style>
 
8
  </script>
9
 
10
  <NodeWithParams {...$$props}>
11
+ {#if data.display}
12
+ <Chart {init} options={data.display} initOptions={{renderer: 'canvas', width: 250, height: 250}}/>
13
  {/if}
14
  </NodeWithParams>
15
  <style>