Skip to content

Commit 0a7ebec

Browse files
authored
Graphgpt (#11)
* Basic literal support added - ints, floats, ranges, lists * Datetime generator fix + keywords generator support added * Default count and key assignments added + missing generator updates * Random image url added and string_from_csv fixed * ADDRESS keyword literal and mock address data generation added * graphGPT integration * Cache openai responses to prevent behind the scenes rerun of ideation tab * Video tutorial url made remote configurable
1 parent 3e78eb9 commit 0a7ebec

14 files changed

+754
-48
lines changed

Pipfile

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ streamlit-player = "*"
1515
requests = "*"
1616
pandas = "*"
1717
random-address = "*"
18+
openai = "*"
19+
numpy = "*"
1820

1921
[dev-packages]
2022
mock-generators = {editable = true, path = "."}

Pipfile.lock

+374-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mock_generators/app.py

+16-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import streamlit as st
22
from constants import *
3+
from tabs.ideate_tab import ideate_tab
34
from tabs.importing_tab import import_tab
45
from tabs.design_tab import design_tab
56
from tabs.data_importer import data_importer_tab
67
from tabs.tutorial import tutorial_tab
8+
from tabs.getting_help import get_help_tab
9+
710
from config import setup_logging, preload_state, load_generators_to_streamlit
811

912
# SETUP
@@ -22,18 +25,24 @@
2225

2326
# Streamlit runs from top-to-bottom from tabs 1 through 8. This is essentially one giant single page app. Earlier attempt to use Streamlit's multi-page app functionality resulted in an inconsistent state between pages.
2427

25-
t0, t1, t2, t5 = st.tabs([
26-
"⓪ Tutorial",
27-
"① Design",
28-
"② Generate",
29-
"③ Data Importer"
28+
t0, t1, t2, t3, t4, t5 = st.tabs([
29+
"⓪ Getting Started",
30+
"① Ideate",
31+
"② Design",
32+
"③ Generate",
33+
"④ Data Importer",
34+
"Ⓘ Info"
3035
])
3136

3237
with t0:
3338
tutorial_tab()
3439
with t1:
35-
design_tab()
40+
ideate_tab()
3641
with t2:
42+
design_tab()
43+
with t3:
3744
import_tab()
45+
with t4:
46+
data_importer_tab()
3847
with t5:
39-
data_importer_tab()
48+
get_help_tab()
+7-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
from random_address import real_random_address_by_state
1+
from random_address import real_random_address
22
import random
33

44
def generate(args: list[any]):
55
# Generate a dictionary with valid random address information
6-
states = [
7-
"AL", "AR", "CA", "CO", "CT", "DC", "FL", "GA", "HI", "KY", "MA" "MD", "TN", "TX", "OK", "VT"
8-
]
9-
state_code = random.choice(states)
10-
return real_random_address_by_state(state_code)
6+
# states = [
7+
# "AL", "AR", "CA", "CO", "CT", "DC", "FL", "GA", "HI", "KY", "MA" "MD", "TN", "TX", "OK", "VT"
8+
# ]
9+
# state_code = random.choice(states)
10+
# return real_random_address_by_state(state_code)
11+
return real_random_address()
+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# For converting data from various forms to other forms
2+
from streamlit_agraph import agraph, Node, Edge, Config
3+
import logging
4+
import json
5+
6+
def random_coordinates_for(
7+
number: int):
8+
import numpy as np
9+
10+
# Generate random coordinates
11+
min_range = 0
12+
max_width = number * 300
13+
x_values = np.random.uniform(min_range, max_width, number)
14+
y_values = np.random.uniform(min_range, max_width, number)
15+
16+
# Create list of (x, y) coordinates
17+
coordinates = list(zip(x_values, y_values))
18+
return coordinates
19+
20+
21+
def agraph_data_from_response(response: str)->tuple[any, any, any]:
22+
# Returns agraph compatible nodes, edges, and config
23+
logging.debug(f'Response: {response}')
24+
# Response will be a list of 3 item tuples
25+
try:
26+
answers = json.loads(response)
27+
except Exception as e:
28+
logging.error(e)
29+
return None, None, None
30+
if isinstance(answers, list) == False:
31+
logging.error(f'Response could not be converted to list, got {type(answers)} instead.')
32+
return None, None, None
33+
nodes = []
34+
# node_labels = []
35+
edges = []
36+
for idx, item in enumerate(answers):
37+
if item is None or len(item) != 3:
38+
continue
39+
n1_label = item[0]
40+
r = item[1]
41+
n2_label = item[2]
42+
43+
# We only want to add agraph nodes with the same label once in our return
44+
45+
# Gross but works
46+
add_n1 = True
47+
for node in nodes:
48+
if node.label == n1_label:
49+
add_n1 = False
50+
if add_n1:
51+
nodes.append(Node(id=n1_label, label=n1_label))
52+
53+
add_n2 = True
54+
for node in nodes:
55+
if node.label == n2_label:
56+
add_n2 = False
57+
if add_n2:
58+
nodes.append(Node(id=n2_label, label=n2_label))
59+
60+
# agraph requires source and target ids to use what we consider labels
61+
edge = Edge(source=n1_label, target=n2_label, label=r)
62+
edges.append(edge)
63+
config = Config(
64+
width=800,
65+
height=800,
66+
backgroundColor="#000000",
67+
directed=True)
68+
return (nodes, edges, config)
69+
70+
71+
def convert_agraph_nodes_to_arrows_nodes(
72+
agraph_nodes: list
73+
)-> list[dict]:
74+
# Convert agraph nodes to arrows nodes
75+
arrows_nodes = []
76+
77+
# Generate random coordinates to init new arrows nodes with - since we can't extract the location data from agraph
78+
coordinates = random_coordinates_for(len(agraph_nodes))
79+
80+
for nidx, node in enumerate(agraph_nodes):
81+
new_node = convert_agraph_node_to_arrows_node(
82+
nidx, node, coordinates[nidx][0], coordinates[nidx][1])
83+
arrows_nodes.append(new_node)
84+
return arrows_nodes
85+
86+
def convert_agraph_node_to_arrows_node(
87+
idx,
88+
node,
89+
x,
90+
y):
91+
# Convert agraph node to arrows node
92+
arrows_node = {
93+
"id": f'n{idx+1}',
94+
"caption": node.label,
95+
"position": {
96+
"x": x,
97+
"y": y,
98+
},
99+
"labels":[],
100+
"style": {},
101+
"properties": {}
102+
}
103+
return arrows_node
104+
105+
def convert_agraph_edge_to_arrows_relationship(
106+
idx,
107+
edge,
108+
arrows_nodes: list):
109+
# Example: {'source': 'People', 'from': 'People', 'to': 'Cars', 'color': '#F7A7A6', 'label': 'DRIVE'}
110+
source_node_label = edge.source
111+
target_node_label = edge.to
112+
source_node_id = None
113+
target_node_id = None
114+
115+
for node in arrows_nodes:
116+
if node['caption'] == source_node_label:
117+
source_node_id = node['id']
118+
if node['caption'] == target_node_label:
119+
target_node_id = node['id']
120+
121+
if source_node_id is None or target_node_id is None:
122+
node_info = [node.__dict__ for node in arrows_nodes]
123+
logging.error(f'Could not find source or target node for edge {edge.__dict__} from nodes: {node_info}')
124+
return None
125+
edge_type = edge.label
126+
arrows_relationship = {
127+
"id": f'n{idx+1}',
128+
"fromId": source_node_id,
129+
"toId": target_node_id,
130+
"type": edge_type,
131+
"properties": {},
132+
"style": {}
133+
}
134+
return arrows_relationship
135+
136+
def convert_agraph_to_arrows(agraph_nodes, agraph_edges):
137+
arrows_nodes = convert_agraph_nodes_to_arrows_nodes(agraph_nodes)
138+
139+
arrows_relationships = []
140+
for eidx, edge in enumerate(agraph_edges):
141+
new_relationship = convert_agraph_edge_to_arrows_relationship(eidx, edge, arrows_nodes=arrows_nodes)
142+
arrows_relationships.append(new_relationship)
143+
arrows_json = {
144+
"nodes": arrows_nodes,
145+
"relationships": arrows_relationships,
146+
"style": {
147+
"font-family": "sans-serif",
148+
"background-color": "#ffffff",
149+
"background-image": "",
150+
"background-size": "100%",
151+
"node-color": "#ffffff",
152+
"border-width": 4,
153+
"border-color": "#000000",
154+
"radius": 50,
155+
"node-padding": 5,
156+
"node-margin": 2,
157+
"outside-position": "auto",
158+
"node-icon-image": "",
159+
"node-background-image": "",
160+
"icon-position": "inside",
161+
"icon-size": 64,
162+
"caption-position": "inside",
163+
"caption-max-width": 200,
164+
"caption-color": "#000000",
165+
"caption-font-size": 50,
166+
"caption-font-weight": "normal",
167+
"label-position": "inside",
168+
"label-display": "pill",
169+
"label-color": "#000000",
170+
"label-background-color": "#ffffff",
171+
"label-border-color": "#000000",
172+
"label-border-width": 4,
173+
"label-font-size": 40,
174+
"label-padding": 5,
175+
"label-margin": 4,
176+
"directionality": "directed",
177+
"detail-position": "inline",
178+
"detail-orientation": "parallel",
179+
"arrow-width": 5,
180+
"arrow-color": "#000000",
181+
"margin-start": 5,
182+
"margin-end": 5,
183+
"margin-peer": 20,
184+
"attachment-start": "normal",
185+
"attachment-end": "normal",
186+
"relationship-icon-image": "",
187+
"type-color": "#000000",
188+
"type-background-color": "#ffffff",
189+
"type-border-color": "#000000",
190+
"type-border-width": 0,
191+
"type-font-size": 16,
192+
"type-padding": 5,
193+
"property-position": "outside",
194+
"property-alignment": "colon",
195+
"property-color": "#000000",
196+
"property-font-size": 16,
197+
"property-font-weight": "normal"
198+
}
199+
}
200+
return arrows_json
201+
22.4 KB
Loading
Loading
Loading

mock_generators/tabs/design_tab.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,21 @@ def design_tab():
1818
2. Select a Node or Relationship to display a properties inspector on the right
1919
3. Configure the mock graph generator by adding properties with specially formatted keys and value strings. See additional details on how to do this in the dropdowns below
2020
4. Once completed, click on the 'Download/Export' button in the arrows.app. Make sure to use the 'JSON' export option
21-
5. Proceed to the ' Generate' tab
21+
5. Proceed to the ' Generate' tab
2222
""")
2323
d1, d2, d3 = st.columns(3)
2424
with d1:
25-
with st.expander("NODE requirements"):
26-
st.write("Nodes require 2 keys to have mock data generated for it: {count} and {key}. The {count} key identifies which data generator (and args) to use for creating the number of nodes.\n\nExample of node properties that would create mock data:")
27-
st.image("mock_generators/media/sample_node_properties.png")
25+
with st.expander("NODE Options"):
26+
st.write("Add a COUNT key to identify how many of a given node you'd like created. Use a KEY key to notate which other property field you'd like use a unique identifier for a node type.\n\nExample of node properties for specifying specific number of nodes with particular properties:")
27+
st.image("mock_generators/media/sample_node_0-5-0.png")
2828
with d2:
29-
with st.expander("RELATIONSHIP requirements"):
30-
st.write("Relationships require 1 key: {count} and can take 2 optional keys: {assignment} and {filter}. The {count} key identifies which data generator (and args) to use for creating the number of relationships between a source node and a target node. EXAMPLE: If every source node should connect to exactly one target node, then the target generator value should be 1.\n\nThe {assignement} key identifies which data generator (and args) to use when deciding which target nodes this relationship should connect with.\n\nThe {filter} key identifies a data generator (and args) to use for deciding what, if any, target nodes to ignore.\n\nExample of node properties that would create mock data:")
31-
st.image("mock_generators/media/sample_relationship_properties.png")
29+
with st.expander("RELATIONSHIP Options"):
30+
st.write("Relationships can take 2 optional keys: COUNT and ASSIGNMENT to specify how many of a given relationship should be created and what assignment generator should be used")
31+
st.image("mock_generators/media/sample_relationship_0-5-0.png")
3232
with d3:
33-
with st.expander("PROPERTY requirements"):
34-
st.write("Properties needing mock generated data should be a stringified JSON object. The unique generator name should be used as a key, followed by a list/array or argument values.\n\nSee the NODE and RELATIONSHIP properties dropdown for examples.\n\nThe right hand Generators preview lists all the available mock data generators. Arguments can be set and example output data can be previewed by clicking on the 'Generate Example Output' button. Use the 'Copy for Arrows' button to copy the required formatted JSON string to your clipboard, to paste into the arrows.app")
35-
st.image("mock_generators/media/sample_generator.png")
33+
with st.expander("PROPERTY Options"):
34+
st.write("Properties needing mock generated data can be a stringified JSON object representing a generator specification. Literals such as numbers, number ranges, and list of options can also be used. For example: 1, 1.0, 1-10, 3.3-4.5, [1,2,3], [1.0, 2.2, 3.3], a_word, [word1, word2, word3] are valid values. General types can also be used, such as string, integer, float, boolean, and datetime.")
35+
st.image("mock_generators/media/sample_properties_0-5-0.png")
3636

3737

3838
c1, c2 = st.columns([8,2])

mock_generators/tabs/getting_help.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import streamlit as st
2+
3+
def get_help_tab():
4+
st.write("Version 0.5.0")
5+
st.write("Email jason.koo@neo4j.com for help.")

0 commit comments

Comments
 (0)