Spaces:
Running
Running
Commit
·
d774d12
1
Parent(s):
75425d3
Add app
Browse files- Dockerfile +31 -0
- LICENSE +21 -0
- README.md +1 -0
- app.py +173 -0
- packages.txt +1 -0
- requirements.txt +4 -0
Dockerfile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11
|
| 2 |
+
|
| 3 |
+
RUN apt-get update
|
| 4 |
+
RUN apt-get --yes install graphviz
|
| 5 |
+
|
| 6 |
+
# Set up a new user named "user" with user ID 1000
|
| 7 |
+
RUN useradd -m -u 1000 user
|
| 8 |
+
|
| 9 |
+
# Switch to the "user" user
|
| 10 |
+
USER user
|
| 11 |
+
|
| 12 |
+
# Set home to the user's home directory
|
| 13 |
+
ENV HOME=/home/user \
|
| 14 |
+
PATH=/home/user/.local/bin:$PATH
|
| 15 |
+
|
| 16 |
+
# Set the working directory to the user's home directory
|
| 17 |
+
WORKDIR $HOME/app
|
| 18 |
+
|
| 19 |
+
# Try and run pip command after setting the user with `USER user` to avoid permission issues with Python
|
| 20 |
+
RUN pip install --no-cache-dir --upgrade pip
|
| 21 |
+
|
| 22 |
+
# Copy the current directory contents into the container at $HOME/app setting the owner to the user
|
| 23 |
+
COPY --chown=user . $HOME/app
|
| 24 |
+
|
| 25 |
+
COPY --chown=user requirements.txt .
|
| 26 |
+
|
| 27 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 28 |
+
|
| 29 |
+
COPY --chown=user app.py app.py
|
| 30 |
+
|
| 31 |
+
ENTRYPOINT ["solara", "run", "app.py", "--host=0.0.0.0", "--port", "7860"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024 Alonso Silva Allende
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -6,6 +6,7 @@ colorTo: gray
|
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
+
app_port: 7860
|
| 10 |
---
|
| 11 |
|
| 12 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import solara
|
| 2 |
+
from typing import Any, Callable, Optional, TypeVar, Union, cast, overload, List
|
| 3 |
+
from typing_extensions import TypedDict
|
| 4 |
+
import time
|
| 5 |
+
import ipyvue
|
| 6 |
+
import reacton
|
| 7 |
+
from solara.alias import rv as v
|
| 8 |
+
import os
|
| 9 |
+
import openai
|
| 10 |
+
from openai import OpenAI
|
| 11 |
+
import instructor
|
| 12 |
+
from pydantic import BaseModel, Field
|
| 13 |
+
from graphviz import Digraph
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# NEEDED FOR INPUT TEXT AREA INSTEAD OF INPUT TEXT
|
| 17 |
+
def use_change(el: reacton.core.Element, on_value: Callable[[Any], Any], enabled=True):
|
| 18 |
+
"""Trigger a callback when a blur events occurs or the enter key is pressed."""
|
| 19 |
+
on_value_ref = solara.use_ref(on_value)
|
| 20 |
+
on_value_ref.current = on_value
|
| 21 |
+
def add_events():
|
| 22 |
+
def on_change(widget, event, data):
|
| 23 |
+
if enabled:
|
| 24 |
+
on_value_ref.current(widget.v_model)
|
| 25 |
+
widget = cast(ipyvue.VueWidget, solara.get_widget(el))
|
| 26 |
+
if enabled:
|
| 27 |
+
widget.on_event("blur", on_change)
|
| 28 |
+
widget.on_event("keyup.enter", on_change)
|
| 29 |
+
def cleanup():
|
| 30 |
+
if enabled:
|
| 31 |
+
widget.on_event("blur", on_change, remove=True)
|
| 32 |
+
widget.on_event("keyup.enter", on_change, remove=True)
|
| 33 |
+
return cleanup
|
| 34 |
+
solara.use_effect(add_events, [enabled])
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@solara.component
|
| 38 |
+
def InputTextarea(
|
| 39 |
+
label: str,
|
| 40 |
+
value: Union[str, solara.Reactive[str]] = "",
|
| 41 |
+
on_value: Callable[[str], None] = None,
|
| 42 |
+
disabled: bool = False,
|
| 43 |
+
password: bool = False,
|
| 44 |
+
continuous_update: bool = False,
|
| 45 |
+
error: Union[bool, str] = False,
|
| 46 |
+
message: Optional[str] = None,
|
| 47 |
+
):
|
| 48 |
+
reactive_value = solara.use_reactive(value, on_value)
|
| 49 |
+
del value, on_value
|
| 50 |
+
def set_value_cast(value):
|
| 51 |
+
reactive_value.value = str(value)
|
| 52 |
+
def on_v_model(value):
|
| 53 |
+
if continuous_update:
|
| 54 |
+
set_value_cast(value)
|
| 55 |
+
messages = []
|
| 56 |
+
if error and isinstance(error, str):
|
| 57 |
+
messages.append(error)
|
| 58 |
+
elif message:
|
| 59 |
+
messages.append(message)
|
| 60 |
+
text_area = v.Textarea(
|
| 61 |
+
v_model=reactive_value.value,
|
| 62 |
+
on_v_model=on_v_model,
|
| 63 |
+
label=label,
|
| 64 |
+
disabled=disabled,
|
| 65 |
+
type="password" if password else None,
|
| 66 |
+
error=bool(error),
|
| 67 |
+
messages=messages,
|
| 68 |
+
solo=True,
|
| 69 |
+
hide_details=True,
|
| 70 |
+
outlined=True,
|
| 71 |
+
rows=1,
|
| 72 |
+
auto_grow=True,
|
| 73 |
+
)
|
| 74 |
+
use_change(text_area, set_value_cast, enabled=not continuous_update)
|
| 75 |
+
return text_area
|
| 76 |
+
|
| 77 |
+
# EXTRACTION
|
| 78 |
+
openai.api_key = os.environ['OPENAI_API_KEY']
|
| 79 |
+
client = instructor.from_openai(OpenAI())
|
| 80 |
+
|
| 81 |
+
class Node(BaseModel):
|
| 82 |
+
id: int
|
| 83 |
+
label: str
|
| 84 |
+
color: str
|
| 85 |
+
|
| 86 |
+
class Edge(BaseModel):
|
| 87 |
+
source: int
|
| 88 |
+
target: int
|
| 89 |
+
label: str
|
| 90 |
+
color: str = "black"
|
| 91 |
+
|
| 92 |
+
class KnowledgeGraph(BaseModel):
|
| 93 |
+
nodes: List[Node] = Field(description="Nodes in the knowledge graph")
|
| 94 |
+
edges: List[Edge] = Field(description="Edges in the knowledge graph")
|
| 95 |
+
|
| 96 |
+
class MessageDict(TypedDict):
|
| 97 |
+
role: str
|
| 98 |
+
content: str
|
| 99 |
+
|
| 100 |
+
def add_chunk_to_ai_message(chunk: str):
|
| 101 |
+
messages.value = [
|
| 102 |
+
*messages.value[:-1],
|
| 103 |
+
{
|
| 104 |
+
"role": "assistant",
|
| 105 |
+
"content": chunk,
|
| 106 |
+
},
|
| 107 |
+
]
|
| 108 |
+
|
| 109 |
+
import ast
|
| 110 |
+
|
| 111 |
+
# DISPLAYED OUTPUT
|
| 112 |
+
@solara.component
|
| 113 |
+
def ChatInterface():
|
| 114 |
+
with solara.lab.ChatBox():
|
| 115 |
+
if len(messages.value)>0:
|
| 116 |
+
if messages.value[-1]["role"] != "user":
|
| 117 |
+
obj = messages.value[-1]["content"]
|
| 118 |
+
if f"{obj}" != "":
|
| 119 |
+
obj = ast.literal_eval(f"{obj}")
|
| 120 |
+
dot = Digraph(comment="Knowledge Graph")
|
| 121 |
+
if obj['nodes'] not in [None, []]:
|
| 122 |
+
if obj['nodes'][0]['label'] not in [None, '']:
|
| 123 |
+
for i, node in enumerate(obj['nodes']):
|
| 124 |
+
if obj['nodes'][i]['label'] not in [None, '']:
|
| 125 |
+
dot.node(name=str(obj['nodes'][i]['id']), label=obj['nodes'][i]['label'], color=obj['nodes'][i]['color'])
|
| 126 |
+
if obj['edges'] not in [None, []]:
|
| 127 |
+
if obj['edges'][0]['label'] not in [None, '']:
|
| 128 |
+
for i, edge in enumerate(obj['edges']):
|
| 129 |
+
if obj['edges'][i]['source'] not in [None,''] and obj['edges'][i]['target'] not in [None,''] and obj['edges'][i]['label'] not in [None,'']:
|
| 130 |
+
dot.edge(str(obj['edges'][i]['source']), str(obj['edges'][i]['target']), label=obj['edges'][i]['label'], color=obj['edges'][i]['color'])
|
| 131 |
+
solara.display(dot)
|
| 132 |
+
|
| 133 |
+
messages: solara.Reactive[List[MessageDict]] = solara.reactive([])
|
| 134 |
+
aux = solara.reactive("")
|
| 135 |
+
text_block = solara.reactive("Alice loves Bob while Charles hates both Alice and Bob.")
|
| 136 |
+
@solara.component
|
| 137 |
+
def Page():
|
| 138 |
+
title = "Knowledge Graph Generator"
|
| 139 |
+
with solara.Head():
|
| 140 |
+
solara.Title(f"{title}")
|
| 141 |
+
with solara.Column(style={"width": "70%", "padding": "50px"}):
|
| 142 |
+
solara.Markdown(f"#{title}")
|
| 143 |
+
solara.Markdown("Enter some text and the language model will try to describe it as a knowledge graph. Done with :heart: by [alonsosilva](https://twitter.com/alonsosilva)")
|
| 144 |
+
extraction_stream = client.chat.completions.create_partial(
|
| 145 |
+
model="gpt-3.5-turbo",
|
| 146 |
+
response_model=KnowledgeGraph,
|
| 147 |
+
messages=[
|
| 148 |
+
{
|
| 149 |
+
"role": "user",
|
| 150 |
+
"content": f"Help me understand the following by describing it as small knowledge graph: {text_block}",
|
| 151 |
+
},
|
| 152 |
+
],
|
| 153 |
+
temperature=0,
|
| 154 |
+
stream=True,
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
user_message_count = len([m for m in messages.value if m["role"] == "user"])
|
| 158 |
+
def send():
|
| 159 |
+
messages.value = [*messages.value, {"role": "user", "content": "Hello"}]
|
| 160 |
+
def response(message):
|
| 161 |
+
for extraction in extraction_stream:
|
| 162 |
+
obj = extraction.model_dump()
|
| 163 |
+
if f"{obj}" != aux.value:
|
| 164 |
+
add_chunk_to_ai_message(f"{obj}")
|
| 165 |
+
aux.value = f"{obj}"
|
| 166 |
+
def result():
|
| 167 |
+
if messages.value != []:
|
| 168 |
+
response(messages.value[-1]["content"])
|
| 169 |
+
result = solara.lab.use_task(result, dependencies=[user_message_count])
|
| 170 |
+
InputTextarea("Enter text:", value=text_block, continuous_update=True)
|
| 171 |
+
solara.Button(label="Generate Knowledge Graph", on_click=send)
|
| 172 |
+
ChatInterface()
|
| 173 |
+
Page()
|
packages.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
graphviz
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
solara==1.31.0
|
| 2 |
+
openai==1.17.0
|
| 3 |
+
instructor==1.1.0
|
| 4 |
+
graphviz==0.20.3
|