Commit
·
51f2dc1
0
Parent(s):
Initial commit
Browse files- .gitignore +210 -0
- Dockerfile +12 -0
- README.md +10 -0
- main.py +91 -0
- prompts/ner/extract_entities +27 -0
- prompts/ner/extract_relations +22 -0
- prompts/search/create_search_plan +18 -0
- requirements.txt +7 -0
- schemas.py +35 -0
- static/css/index.css +9 -0
- static/index.html +83 -0
- static/js/index.js +112 -0
- utils.py +37 -0
.gitignore
ADDED
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Byte-compiled / optimized / DLL files
|
2 |
+
__pycache__/
|
3 |
+
*.py[codz]
|
4 |
+
*$py.class
|
5 |
+
|
6 |
+
# C extensions
|
7 |
+
*.so
|
8 |
+
|
9 |
+
# Distribution / packaging
|
10 |
+
.Python
|
11 |
+
build/
|
12 |
+
develop-eggs/
|
13 |
+
dist/
|
14 |
+
downloads/
|
15 |
+
eggs/
|
16 |
+
.eggs/
|
17 |
+
lib/
|
18 |
+
lib64/
|
19 |
+
parts/
|
20 |
+
sdist/
|
21 |
+
var/
|
22 |
+
wheels/
|
23 |
+
share/python-wheels/
|
24 |
+
*.egg-info/
|
25 |
+
.installed.cfg
|
26 |
+
*.egg
|
27 |
+
MANIFEST
|
28 |
+
|
29 |
+
# PyInstaller
|
30 |
+
# Usually these files are written by a python script from a template
|
31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
32 |
+
*.manifest
|
33 |
+
*.spec
|
34 |
+
|
35 |
+
# Installer logs
|
36 |
+
pip-log.txt
|
37 |
+
pip-delete-this-directory.txt
|
38 |
+
|
39 |
+
# Unit test / coverage reports
|
40 |
+
htmlcov/
|
41 |
+
.tox/
|
42 |
+
.nox/
|
43 |
+
.coverage
|
44 |
+
.coverage.*
|
45 |
+
.cache
|
46 |
+
nosetests.xml
|
47 |
+
coverage.xml
|
48 |
+
*.cover
|
49 |
+
*.py.cover
|
50 |
+
.hypothesis/
|
51 |
+
.pytest_cache/
|
52 |
+
cover/
|
53 |
+
|
54 |
+
# Translations
|
55 |
+
*.mo
|
56 |
+
*.pot
|
57 |
+
|
58 |
+
# Django stuff:
|
59 |
+
*.log
|
60 |
+
local_settings.py
|
61 |
+
db.sqlite3
|
62 |
+
db.sqlite3-journal
|
63 |
+
|
64 |
+
# Flask stuff:
|
65 |
+
instance/
|
66 |
+
.webassets-cache
|
67 |
+
|
68 |
+
# Scrapy stuff:
|
69 |
+
.scrapy
|
70 |
+
|
71 |
+
# Sphinx documentation
|
72 |
+
docs/_build/
|
73 |
+
|
74 |
+
# PyBuilder
|
75 |
+
.pybuilder/
|
76 |
+
target/
|
77 |
+
|
78 |
+
# Jupyter Notebook
|
79 |
+
.ipynb_checkpoints
|
80 |
+
|
81 |
+
# IPython
|
82 |
+
profile_default/
|
83 |
+
ipython_config.py
|
84 |
+
|
85 |
+
# pyenv
|
86 |
+
# For a library or package, you might want to ignore these files since the code is
|
87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
88 |
+
# .python-version
|
89 |
+
|
90 |
+
# pipenv
|
91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
94 |
+
# install all needed dependencies.
|
95 |
+
#Pipfile.lock
|
96 |
+
|
97 |
+
# UV
|
98 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
100 |
+
# commonly ignored for libraries.
|
101 |
+
#uv.lock
|
102 |
+
|
103 |
+
# poetry
|
104 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
105 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
106 |
+
# commonly ignored for libraries.
|
107 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
108 |
+
#poetry.lock
|
109 |
+
#poetry.toml
|
110 |
+
|
111 |
+
# pdm
|
112 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
113 |
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
114 |
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
115 |
+
#pdm.lock
|
116 |
+
#pdm.toml
|
117 |
+
.pdm-python
|
118 |
+
.pdm-build/
|
119 |
+
|
120 |
+
# pixi
|
121 |
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
122 |
+
#pixi.lock
|
123 |
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
124 |
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
125 |
+
.pixi
|
126 |
+
|
127 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
128 |
+
__pypackages__/
|
129 |
+
|
130 |
+
# Celery stuff
|
131 |
+
celerybeat-schedule
|
132 |
+
celerybeat.pid
|
133 |
+
|
134 |
+
# SageMath parsed files
|
135 |
+
*.sage.py
|
136 |
+
|
137 |
+
# Environments
|
138 |
+
.env
|
139 |
+
.envrc
|
140 |
+
.venv
|
141 |
+
env/
|
142 |
+
venv/
|
143 |
+
ENV/
|
144 |
+
env.bak/
|
145 |
+
venv.bak/
|
146 |
+
|
147 |
+
# Spyder project settings
|
148 |
+
.spyderproject
|
149 |
+
.spyproject
|
150 |
+
|
151 |
+
# Rope project settings
|
152 |
+
.ropeproject
|
153 |
+
|
154 |
+
# mkdocs documentation
|
155 |
+
/site
|
156 |
+
|
157 |
+
# mypy
|
158 |
+
.mypy_cache/
|
159 |
+
.dmypy.json
|
160 |
+
dmypy.json
|
161 |
+
|
162 |
+
# Pyre type checker
|
163 |
+
.pyre/
|
164 |
+
|
165 |
+
# pytype static type analyzer
|
166 |
+
.pytype/
|
167 |
+
|
168 |
+
# Cython debug symbols
|
169 |
+
cython_debug/
|
170 |
+
|
171 |
+
# PyCharm
|
172 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
173 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
174 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
175 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
176 |
+
#.idea/
|
177 |
+
|
178 |
+
# Abstra
|
179 |
+
# Abstra is an AI-powered process automation framework.
|
180 |
+
# Ignore directories containing user credentials, local state, and settings.
|
181 |
+
# Learn more at https://abstra.io/docs
|
182 |
+
.abstra/
|
183 |
+
|
184 |
+
# Visual Studio Code
|
185 |
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
186 |
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
187 |
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
188 |
+
# you could uncomment the following to ignore the entire vscode folder
|
189 |
+
# .vscode/
|
190 |
+
|
191 |
+
# Ruff stuff:
|
192 |
+
.ruff_cache/
|
193 |
+
|
194 |
+
# PyPI configuration file
|
195 |
+
.pypirc
|
196 |
+
|
197 |
+
# Cursor
|
198 |
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
199 |
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
200 |
+
# refer to https://docs.cursor.com/context/ignore-files
|
201 |
+
.cursorignore
|
202 |
+
.cursorindexingignore
|
203 |
+
|
204 |
+
# Marimo
|
205 |
+
marimo/_static/
|
206 |
+
marimo/_lsp/
|
207 |
+
__marimo__/
|
208 |
+
|
209 |
+
# Streamlit
|
210 |
+
.streamlit/secrets.toml
|
Dockerfile
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.11-slim
|
2 |
+
|
3 |
+
WORKDIR /app
|
4 |
+
|
5 |
+
COPY requirements.txt .
|
6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
7 |
+
|
8 |
+
COPY . .
|
9 |
+
|
10 |
+
EXPOSE 7860
|
11 |
+
|
12 |
+
ENTRYPOINT ["uvicorn", "main:api", "--port", "7860", "--host", "0.0.0.0"]
|
README.md
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: Graphify
|
3 |
+
emoji: 🏢
|
4 |
+
colorFrom: yellow
|
5 |
+
colorTo: red
|
6 |
+
sdk: docker
|
7 |
+
pinned: false
|
8 |
+
license: mit
|
9 |
+
short_description: Transform data into knowledge graphs
|
10 |
+
---
|
main.py
ADDED
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import sys
|
3 |
+
from litellm import acompletion
|
4 |
+
from dotenv import load_dotenv
|
5 |
+
from fastapi import FastAPI
|
6 |
+
from fastapi.staticfiles import StaticFiles
|
7 |
+
from jinja2 import Environment, FileSystemLoader, StrictUndefined, TemplateNotFound
|
8 |
+
from schemas import CreateSearchPlanRequest, CreateSearchPlanResponse, ExtractEntitiesRequest, ExtractEntitiesResponse, ExtractedRelationsResponse
|
9 |
+
from utils import build_visjs_graph, fmt_prompt
|
10 |
+
import logging
|
11 |
+
|
12 |
+
load_dotenv()
|
13 |
+
|
14 |
+
logging.basicConfig(
|
15 |
+
level=logging.INFO,
|
16 |
+
format='[%(asctime)s][%(levelname)s][%(filename)s:%(lineno)d]: %(message)s',
|
17 |
+
datefmt='%Y-%m-%d %H:%M:%S'
|
18 |
+
)
|
19 |
+
|
20 |
+
|
21 |
+
LLM_MODEL = os.environ.get('LLM_MODEL', default=None)
|
22 |
+
LLM_TOKEN = os.environ.get('LLM_TOKEN', default=None)
|
23 |
+
LLM_BASE_URL = os.environ.get('LLM_BASE_URL', default=None)
|
24 |
+
|
25 |
+
if not LLM_MODEL and not LLM_TOKEN:
|
26 |
+
logging.error("No LLM_TOKEN and LLM_MODEL were provided.")
|
27 |
+
sys.exit(-1)
|
28 |
+
|
29 |
+
prompt_env = Environment(loader=FileSystemLoader(
|
30 |
+
"prompts"), undefined=StrictUndefined, enable_async=True)
|
31 |
+
|
32 |
+
api = FastAPI()
|
33 |
+
|
34 |
+
|
35 |
+
@api.post("/extract_entities")
|
36 |
+
async def extract_entities(body: ExtractEntitiesRequest):
|
37 |
+
"""Extract entities from the given input text and return them"""
|
38 |
+
# Extract entities from the text
|
39 |
+
entities_completion = await acompletion(LLM_MODEL, api_key=LLM_TOKEN, base_url=LLM_BASE_URL, messages=[
|
40 |
+
{
|
41 |
+
"role": "user",
|
42 |
+
"content": await fmt_prompt(prompt_env, "ner/extract_entities", **{
|
43 |
+
"response_format": ExtractEntitiesResponse.model_json_schema(),
|
44 |
+
"input_text": body.content
|
45 |
+
})
|
46 |
+
}
|
47 |
+
], response_format=ExtractEntitiesResponse)
|
48 |
+
|
49 |
+
extracted_entities = ExtractEntitiesResponse.model_validate_json(
|
50 |
+
entities_completion.choices[0].message.content)
|
51 |
+
|
52 |
+
# Extract relationships in a second step
|
53 |
+
relations_completion = await acompletion(LLM_MODEL, api_key=LLM_TOKEN, base_url=LLM_BASE_URL, messages=[
|
54 |
+
{
|
55 |
+
"role": "user",
|
56 |
+
"content": await fmt_prompt(prompt_env, "ner/extract_relations", **{
|
57 |
+
"response_format": ExtractedRelationsResponse.model_json_schema(),
|
58 |
+
"input_text": body.content,
|
59 |
+
"entities": extracted_entities.entities
|
60 |
+
})
|
61 |
+
}
|
62 |
+
], response_format=ExtractedRelationsResponse, num_retries=5)
|
63 |
+
|
64 |
+
relation_model = ExtractedRelationsResponse.model_validate_json(
|
65 |
+
relations_completion.choices[0].message.content)
|
66 |
+
|
67 |
+
display_lists = build_visjs_graph(
|
68 |
+
extracted_entities.entities, relation_model.relations)
|
69 |
+
|
70 |
+
return display_lists
|
71 |
+
|
72 |
+
|
73 |
+
@api.post("/create_search_plan")
|
74 |
+
async def create_search_plan(body: CreateSearchPlanRequest):
|
75 |
+
plan_completion = await acompletion(LLM_MODEL, api_key=LLM_TOKEN, base_url=LLM_BASE_URL, messages=[
|
76 |
+
{
|
77 |
+
"role": "user",
|
78 |
+
"content": await fmt_prompt(prompt_env, "search/create_search_plan", **{
|
79 |
+
"response_format": CreateSearchPlanResponse.model_json_schema(),
|
80 |
+
"user_query": body.query,
|
81 |
+
})
|
82 |
+
}
|
83 |
+
], response_format=CreateSearchPlanResponse)
|
84 |
+
|
85 |
+
plan_model = CreateSearchPlanResponse.model_validate_json(
|
86 |
+
plan_completion.choices[0].message.content)
|
87 |
+
|
88 |
+
return plan_model
|
89 |
+
|
90 |
+
|
91 |
+
api.mount("/", StaticFiles(directory="static", html=True), name="static")
|
prompts/ner/extract_entities
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<role>You are a useful search assistant. </role>
|
2 |
+
<task>
|
3 |
+
Extract all the entities that appear in the following given input texxt
|
4 |
+
An entity may refer to:
|
5 |
+
- A person (eg. Marie Curie)
|
6 |
+
- An organisation (eg. Google Inc, United Nations)
|
7 |
+
- A date (eg. July 14, 2025)
|
8 |
+
- A fact
|
9 |
+
- An event (eg. WWII)
|
10 |
+
- An acronym (eg.NASA)
|
11 |
+
- A location (eg. NY City)
|
12 |
+
- A product (eg. ChatGPT, Iphone 14)
|
13 |
+
- A quantity, money or percentage
|
14 |
+
- A technical term or domain concept
|
15 |
+
</task>
|
16 |
+
|
17 |
+
<response_format>
|
18 |
+
Reply in JSON with the following reponse schema:
|
19 |
+
{{response_format}}
|
20 |
+
</response_format>
|
21 |
+
|
22 |
+
|
23 |
+
<input_text>
|
24 |
+
Here is the text:
|
25 |
+
|
26 |
+
{{input_text}}
|
27 |
+
</input_text>
|
prompts/ner/extract_relations
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<role>You are a useful search assistant. </role>
|
2 |
+
<task>
|
3 |
+
Extract all the relations described in the following text for the following list of entities.
|
4 |
+
Please provide relations for the entities listed under.
|
5 |
+
</task>
|
6 |
+
|
7 |
+
<entities>
|
8 |
+
Here is the list of entities:
|
9 |
+
{% for entity in entities -%}
|
10 |
+
- {{entity}}
|
11 |
+
{% endfor %}
|
12 |
+
</entities>
|
13 |
+
|
14 |
+
<response_format>
|
15 |
+
Reply in JSON with the following response schema:
|
16 |
+
{{response_format}}
|
17 |
+
</response_format>
|
18 |
+
|
19 |
+
<input_text>
|
20 |
+
Here is the text:
|
21 |
+
{{input_text}}
|
22 |
+
</input_text>
|
prompts/search/create_search_plan
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<role>You are a useful search assistant. </role>
|
2 |
+
<task>
|
3 |
+
You are tasked with creating a comprehensive search plan to explore a complex query given by the user under.
|
4 |
+
Your goal is to break down the query into multiple sub-queries that represents dimensions that must be explored for
|
5 |
+
a holistic understanding of
|
6 |
+
the initial query.
|
7 |
+
</task>
|
8 |
+
|
9 |
+
<response_format>
|
10 |
+
Only provide the sub-queries formulated as questions.
|
11 |
+
Provide the answer as a JSON object using the following response schema:
|
12 |
+
{{response_format}}
|
13 |
+
</response_format>
|
14 |
+
|
15 |
+
<topic>
|
16 |
+
The user query is:
|
17 |
+
**{{user_query}}**
|
18 |
+
</topic>
|
requirements.txt
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
litellm
|
2 |
+
pydantic
|
3 |
+
fastapi
|
4 |
+
uvicorn
|
5 |
+
networkx
|
6 |
+
Jinja2
|
7 |
+
dotenv
|
schemas.py
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pydantic import BaseModel, Field
|
2 |
+
|
3 |
+
|
4 |
+
# ============================================= Entity + Relations extraction
|
5 |
+
|
6 |
+
class ExtractEntitiesRequest(BaseModel):
|
7 |
+
content: str
|
8 |
+
|
9 |
+
|
10 |
+
class ExtractEntitiesResponse(BaseModel):
|
11 |
+
entities: list[str] = Field(..., description="A list of entities")
|
12 |
+
|
13 |
+
|
14 |
+
class ExtractedRelation(BaseModel):
|
15 |
+
start: str = Field(..., description="The first entity in the relationship")
|
16 |
+
to: str = Field(..., description="The second entity of the relationship")
|
17 |
+
tag: str = Field(..., description="A tag describing the relationship", examples=[
|
18 |
+
"related_to", "born_in", "made", "created"])
|
19 |
+
description: str = Field(...,
|
20 |
+
description="A detailled description of the relationship")
|
21 |
+
|
22 |
+
|
23 |
+
class ExtractedRelationsResponse(BaseModel):
|
24 |
+
relations: list[ExtractedRelation]
|
25 |
+
|
26 |
+
|
27 |
+
# ======================================================== Create search plan ==================
|
28 |
+
|
29 |
+
class CreateSearchPlanRequest(BaseModel):
|
30 |
+
query: str
|
31 |
+
|
32 |
+
|
33 |
+
class CreateSearchPlanResponse(BaseModel):
|
34 |
+
sub_queries: list[str] = Field(...,
|
35 |
+
description="A list of subqueries formulated as questions")
|
static/css/index.css
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.dot-grid {
|
2 |
+
position: relative;
|
3 |
+
width: 100%;
|
4 |
+
height: 100%;
|
5 |
+
background-image: radial-gradient(rgba(128, 128, 128, 0.4) 1px, transparent 1px);
|
6 |
+
background-size: 12px 12px; /* Adjust spacing between dots */
|
7 |
+
background-repeat: repeat;
|
8 |
+
overflow: hidden;
|
9 |
+
}
|
static/index.html
ADDED
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en" data-theme="dark">
|
3 |
+
|
4 |
+
<head>
|
5 |
+
<meta charset="UTF-8" />
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7 |
+
<title>Chatbot UI</title>
|
8 |
+
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
9 |
+
<link href="css/index.css" rel="stylesheet" type="text/css" />
|
10 |
+
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
11 |
+
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
12 |
+
<script src="js/index.js"></script>
|
13 |
+
</head>
|
14 |
+
|
15 |
+
<body class="bg-base-200 p-4 overflow-hidden">
|
16 |
+
<div class="relative h-[95vh] bg-base-100 rounded-2xl p-5 shadow-xl">
|
17 |
+
|
18 |
+
<!-- tab container-->
|
19 |
+
<div class="absolute -top-2 flex left-1/2 transform -translate-x-1/2 z-10">
|
20 |
+
<div class="tabs tabs-box glass">
|
21 |
+
<button id="chat-tab" class="tab tab-active flex items-center gap-2" onclick="switchTab('chat-tab')">
|
22 |
+
<svg width="16px" height="16px" viewBox="0 0 24 24" fill="none">
|
23 |
+
<g stroke-width="0"></g>
|
24 |
+
<g stroke-linecap="round" stroke-linejoin="round"></g>
|
25 |
+
<g>
|
26 |
+
<path d="M8 10.5H16" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round"></path>
|
27 |
+
<path d="M8 14H13.5" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round"></path>
|
28 |
+
<path
|
29 |
+
d="M17 3.33782C15.5291 2.48697 13.8214 2 12 2C6.47715 2 2 6.47715 2 12C2 13.5997 2.37562 15.1116 3.04346 16.4525C3.22094 16.8088 3.28001 17.2161 3.17712 17.6006L2.58151 19.8267C2.32295 20.793 3.20701 21.677 4.17335 21.4185L6.39939 20.8229C6.78393 20.72 7.19121 20.7791 7.54753 20.9565C8.88837 21.6244 10.4003 22 12 22C17.5228 22 22 17.5228 22 12C22 10.1786 21.513 8.47087 20.6622 7"
|
30 |
+
stroke="#ffffff" stroke-width="1.5" stroke-linecap="round"></path>
|
31 |
+
</g>
|
32 |
+
</svg>
|
33 |
+
Chat
|
34 |
+
</button>
|
35 |
+
<button id="explore-tab" class="tab flex items-center gap-2" onclick="switchTab('explore-tab')">
|
36 |
+
<svg fill="#ffffff" width="16px" height="16px" viewBox="0 -64 640 640" stroke="#ffffff">
|
37 |
+
<g stroke-width="0"></g>
|
38 |
+
<g stroke-linecap="round" stroke-linejoin="round"></g>
|
39 |
+
<g>
|
40 |
+
<path
|
41 |
+
d="M384 320H256c-17.67 0-32 14.33-32 32v128c0 17.67 14.33 32 32 32h128c17.67 0 32-14.33 32-32V352c0-17.67-14.33-32-32-32zM192 32c0-17.67-14.33-32-32-32H32C14.33 0 0 14.33 0 32v128c0 17.67 14.33 32 32 32h95.72l73.16 128.04C211.98 300.98 232.4 288 256 288h.28L192 175.51V128h224V64H192V32zM608 0H480c-17.67 0-32 14.33-32 32v128c0 17.67 14.33 32 32 32h128c17.67 0 32-14.33 32-32V32c0-17.67-14.33-32-32-32z">
|
42 |
+
</path>
|
43 |
+
</g>
|
44 |
+
</svg>
|
45 |
+
Explore
|
46 |
+
</button>
|
47 |
+
</div>
|
48 |
+
</div>
|
49 |
+
|
50 |
+
<!-- Chat UI Container -->
|
51 |
+
<div id="chat-tab-contents" class="relative w-full h-full">
|
52 |
+
<!-- <div class=" flex flex-col overflow-hidden"> -->
|
53 |
+
<div id="chat-messages-container" class="w-full h-full space-y-4 overflow-y-auto flex-1 pr-2 pb-20">
|
54 |
+
<!-- Message Bubble (Bot) -->
|
55 |
+
<div class="chat chat-start">
|
56 |
+
<div class="chat-bubble chat-bubble-secondary">Hello! Give me some data and I'll turn it into a nice KG graph for you</div>
|
57 |
+
</div>
|
58 |
+
</div>
|
59 |
+
|
60 |
+
<!-- Input Area -->
|
61 |
+
<div class="absolute bottom-0 left-0 right-0 p-4 rounded-xl shadow-xl bg-gray-700 text-base-content">
|
62 |
+
<div class="w-full">
|
63 |
+
<div class="flex gap-2 items-end">
|
64 |
+
<textarea id="chat-input"
|
65 |
+
class="w-full bg-base-800 text-base-content border max-h-[25vh] overflow-y-auto resize-y p-2 rounded-md"
|
66 |
+
placeholder="Type your message..." rows="1"></textarea>
|
67 |
+
<button id="chat-send-input" class="btn btn-sm btn-primary rounded-full">Send</button>
|
68 |
+
</div>
|
69 |
+
</div>
|
70 |
+
</div>
|
71 |
+
|
72 |
+
|
73 |
+
<!-- </div> -->
|
74 |
+
</div>
|
75 |
+
|
76 |
+
|
77 |
+
<div class="relative w-full h-full dot-grid overflow-hidden hidden" id="explore-tab-contents">
|
78 |
+
<div id="grapha" class="w-full h-full "></div>
|
79 |
+
</div>
|
80 |
+
|
81 |
+
</div>
|
82 |
+
</body>
|
83 |
+
</html>
|
static/js/index.js
ADDED
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
// mapping of tab to its contents
|
3 |
+
const TABS = {
|
4 |
+
"chat-tab": "chat-tab-contents",
|
5 |
+
"explore-tab": "explore-tab-contents"
|
6 |
+
};
|
7 |
+
|
8 |
+
// switch to the specified tab
|
9 |
+
function switchTab(newTab) {
|
10 |
+
Object.keys(TABS).forEach(tabId => {
|
11 |
+
const tabElement = document.getElementById(tabId);
|
12 |
+
if (tabElement) {
|
13 |
+
tabElement.classList.remove("tab-active");
|
14 |
+
}
|
15 |
+
});
|
16 |
+
|
17 |
+
// Hide all tab contents
|
18 |
+
Object.values(TABS).forEach(contentId => {
|
19 |
+
const contentElement = document.getElementById(contentId);
|
20 |
+
if (contentElement) {
|
21 |
+
contentElement.classList.add("hidden");
|
22 |
+
}
|
23 |
+
});
|
24 |
+
|
25 |
+
// Activate the new tab if it exists in the mapping
|
26 |
+
if (newTab in TABS) {
|
27 |
+
const newTabElement = document.getElementById(newTab);
|
28 |
+
const newContentElement = document.getElementById(TABS[newTab]);
|
29 |
+
|
30 |
+
if (newTabElement) newTabElement.classList.add("tab-active");
|
31 |
+
if (newContentElement) newContentElement.classList.remove("hidden");
|
32 |
+
}
|
33 |
+
}
|
34 |
+
|
35 |
+
function addMessage(message, role = "user") {
|
36 |
+
const chatContainer = document.getElementById("chat-messages-container");
|
37 |
+
|
38 |
+
const chatDiv = document.createElement("div");
|
39 |
+
chatDiv.classList.add("chat");
|
40 |
+
|
41 |
+
if (role === "user")
|
42 |
+
chatDiv.classList.add("chat-end");
|
43 |
+
else if (role === "bot")
|
44 |
+
chatDiv.classList.add("chat-start");
|
45 |
+
else
|
46 |
+
chatDiv.classList.add("chat-start");
|
47 |
+
|
48 |
+
const bubbleDiv = document.createElement("div");
|
49 |
+
bubbleDiv.classList.add("chat-bubble");
|
50 |
+
|
51 |
+
if (role === "user")
|
52 |
+
bubbleDiv.classList.add("chat-bubble-primary");
|
53 |
+
else if (role === "bot")
|
54 |
+
bubbleDiv.classList.add("chat-bubble-secondary");
|
55 |
+
else
|
56 |
+
bubbleDiv.classList.add("chat-bubble-secondary");
|
57 |
+
|
58 |
+
bubbleDiv.textContent = message;
|
59 |
+
|
60 |
+
chatDiv.appendChild(bubbleDiv);
|
61 |
+
chatContainer.appendChild(chatDiv);
|
62 |
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
63 |
+
}
|
64 |
+
|
65 |
+
|
66 |
+
function escapeString(str) {
|
67 |
+
return str
|
68 |
+
.replace(/\\/g, '\\\\') // Escape backslashes
|
69 |
+
.replace(/"/g, '\\"') // Escape double quotes
|
70 |
+
.replace(/\n/g, '\\n') // Escape newlines
|
71 |
+
.replace(/\r/g, '\\r') // Escape carriage returns
|
72 |
+
.replace(/\t/g, '\\t'); // Escape tabs
|
73 |
+
}
|
74 |
+
|
75 |
+
// extract entities from the given input text
|
76 |
+
async function extractEntities(text) {
|
77 |
+
let escaped_text = escapeString(text);
|
78 |
+
|
79 |
+
try {
|
80 |
+
let graph_data_req = await fetch("/extract_entities", {
|
81 |
+
method: "POST",
|
82 |
+
headers: new Headers({
|
83 |
+
"Content-Type": "application/json"
|
84 |
+
}),
|
85 |
+
body: JSON.stringify({
|
86 |
+
content: escaped_text
|
87 |
+
})
|
88 |
+
})
|
89 |
+
|
90 |
+
let graph_data = await graph_data_req.json();
|
91 |
+
|
92 |
+
console.log(graph_data);
|
93 |
+
|
94 |
+
var options = {};
|
95 |
+
var network = new vis.Network(document.getElementById('grapha'), graph_data, options);
|
96 |
+
addMessage("I've created a knowledge graph based on what you've provided me. Check the explore tab !", "bot");
|
97 |
+
} catch (e) {
|
98 |
+
console.error("Error while trying to extract entities into a KG", e);
|
99 |
+
}
|
100 |
+
}
|
101 |
+
|
102 |
+
|
103 |
+
document.addEventListener('DOMContentLoaded', _ => {
|
104 |
+
document.getElementById('chat-send-input').addEventListener('click', (ev) => {
|
105 |
+
let textbox = document.getElementById('chat-input');
|
106 |
+
let text = textbox.value;
|
107 |
+
|
108 |
+
addMessage(text, "user");
|
109 |
+
extractEntities(text);
|
110 |
+
textbox.value = "";
|
111 |
+
});
|
112 |
+
})
|
utils.py
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Dict, List
|
2 |
+
from jinja2 import Environment
|
3 |
+
|
4 |
+
from schemas import ExtractedRelation
|
5 |
+
|
6 |
+
|
7 |
+
def build_visjs_graph(entities: List[str], relations: List[ExtractedRelation]) -> Dict[str, List[Dict]]:
|
8 |
+
"""Builds a vertex and edge graph for displaying in UI"""
|
9 |
+
|
10 |
+
unique_entities = set(entities) # maintains order, removes duplicates
|
11 |
+
entity_to_id = {entity: idx for idx, entity in enumerate(unique_entities)}
|
12 |
+
nodes = [
|
13 |
+
{"id": entity_to_id[entity], "label": entity, "title": entity}
|
14 |
+
for entity in unique_entities
|
15 |
+
]
|
16 |
+
|
17 |
+
# Create edges list from relations
|
18 |
+
edges = []
|
19 |
+
for rel in relations:
|
20 |
+
start_id = entity_to_id.get(rel.start)
|
21 |
+
end_id = entity_to_id.get(rel.to)
|
22 |
+
if start_id is not None and end_id is not None:
|
23 |
+
edges.append({
|
24 |
+
"from": start_id,
|
25 |
+
"to": end_id,
|
26 |
+
"label": rel.tag,
|
27 |
+
"title": rel.description,
|
28 |
+
"arrows": "to",
|
29 |
+
})
|
30 |
+
|
31 |
+
return {"nodes": nodes, "edges": edges}
|
32 |
+
|
33 |
+
|
34 |
+
async def fmt_prompt(env: Environment, prompt_id: str, **args):
|
35 |
+
"""Returns a formatted prompt"""
|
36 |
+
prompt = env.get_template(prompt_id)
|
37 |
+
return await prompt.render_async(args)
|