├── .env.example ├── .gitignore ├── README.md ├── backend ├── .dockerignore ├── Dockerfile ├── poetry.lock ├── pyproject.toml └── src │ ├── agent.py │ ├── cypher_tool.py │ ├── database.py │ ├── endpoints.py │ ├── env.py │ ├── logger.py │ ├── main.py │ └── run.py ├── docker-compose.yml ├── frontend ├── .dockerignore ├── .gitignore ├── Dockerfile ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reportWebVitals.js │ └── setupTests.js └── images ├── chains-flow.png ├── qa-1.png └── qa-2.png /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_KEY=API_KEY 2 | NEO4J_URL=bolt://URL:7687 3 | NEO4J_USER=USER 4 | NEO4J_PASS=PASS 5 | MODEL_NAME=gpt-3.5-turbo -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Mac 10 | .DS_Store 11 | .ruff_cache 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | # Byte-compiled / optimized / DLL files 110 | __pycache__/ 111 | *.py[cod] 112 | *$py.class 113 | notebooks 114 | 115 | # C extensions 116 | *.so 117 | 118 | # Distribution / packaging 119 | .Python 120 | build/ 121 | develop-eggs/ 122 | dist/ 123 | downloads/ 124 | eggs/ 125 | .eggs/ 126 | lib/ 127 | lib64/ 128 | parts/ 129 | sdist/ 130 | var/ 131 | wheels/ 132 | pip-wheel-metadata/ 133 | share/python-wheels/ 134 | *.egg-info/ 135 | .installed.cfg 136 | *.egg 137 | MANIFEST 138 | 139 | # PyInstaller 140 | # Usually these files are written by a python script from a template 141 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 142 | *.manifest 143 | *.spec 144 | 145 | # Installer logs 146 | pip-log.txt 147 | pip-delete-this-directory.txt 148 | 149 | # Unit test / coverage reports 150 | htmlcov/ 151 | .tox/ 152 | .nox/ 153 | .coverage 154 | .coverage.* 155 | .cache 156 | nosetests.xml 157 | coverage.xml 158 | *.cover 159 | *.py,cover 160 | .hypothesis/ 161 | .pytest_cache/ 162 | 163 | # Translations 164 | *.mo 165 | *.pot 166 | 167 | # Django stuff: 168 | *.log 169 | local_settings.py 170 | db.sqlite3 171 | db.sqlite3-journal 172 | 173 | # Flask stuff: 174 | instance/ 175 | .webassets-cache 176 | 177 | # Scrapy stuff: 178 | .scrapy 179 | 180 | # Sphinx documentation 181 | docs/_build/ 182 | 183 | # PyBuilder 184 | target/ 185 | 186 | # Jupyter Notebook 187 | .ipynb_checkpoints 188 | 189 | # IPython 190 | profile_default/ 191 | ipython_config.py 192 | 193 | # pyenv 194 | .python-version 195 | 196 | # pipenv 197 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 198 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 199 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 200 | # install all needed dependencies. 201 | #Pipfile.lock 202 | 203 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 204 | __pypackages__/ 205 | 206 | # Celery stuff 207 | celerybeat-schedule 208 | celerybeat.pid 209 | 210 | # SageMath parsed files 211 | *.sage.py 212 | 213 | # Environments 214 | .env 215 | .venv 216 | env/ 217 | venv/ 218 | ENV/ 219 | env.bak/ 220 | venv.bak/ 221 | 222 | # Spyder project settings 223 | .spyderproject 224 | .spyproject 225 | 226 | # Rope project settings 227 | .ropeproject 228 | 229 | # mkdocs documentation 230 | /site 231 | 232 | # mypy 233 | .mypy_cache/ 234 | .dmypy.json 235 | dmypy.json 236 | 237 | # Poetry 238 | .testenv/* 239 | 240 | movie_embeddings.csv 241 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Langchain2ONgDB 2 | >  LangChain 是一种 LLMs(大语言模型) 接口框架,它允许用户围绕大型语言模型快速构建应用程序和管道。 3 | >`Langchain2ONgDB` 是参考 [Langchain2Neo4j](https://github.com/tomasonjo/langchain2neo4j) 的实验项目,将ONgDB集成到了`LangChain`生态系统。 4 | >在`Langchain2Neo4j`的基础上去掉了`Keyword search(关键词全文检索)`和`Vector search(向量检索)`功能,只保留了基础的`Cypher`生成工具,并跑通了一个 [中文数据集案例](https://github.com/ongdb-contrib/graph-qabot-demo/tree/main/db) 。 5 | >如果对去掉的两个工具感兴趣可以查看 [Langchain2ONgDB历史仓库](https://github.com/ongdb-contrib/langchain2ongdb/tree/main) 。 6 | 7 | ![qa-1](images/qa-1.png) 8 | 9 | ![qa-2](images/qa-2.png) 10 | 11 | 1. [Langchain2ONgDB分支:main](https://github.com/ongdb-contrib/langchain2ongdb/blob/main) ,设置OPENAI_KEY后跑通了英文数据集 12 | 13 | 2. [Langchain2ONgDB分支:1.0](https://github.com/ongdb-contrib/langchain2ongdb/blob/1.0) ,设置OPENAI_KEY后跑通了中文数据集 14 | 15 | 3. [~~Langchain2ONgDB分支:2.0~~](https://github.com/ongdb-contrib/langchain2ongdb/blob/2.0) ~~,设置封装的GPT的HTTP接口后跑通了中文数据集~~ 16 | 17 | ## LangChain代理流程 18 | >  代理流程在接收到用户输入时启动。 19 | > 然后,代理向 LLM 模型发送请求,其中包括用户问题和代理提示,代理提示是代理应该遵循的一组自然语言指令。 20 | > 反过来,LLM 会向代理人提供进一步的指示。 21 | > 对于工具的使用大多数情况下,我们的第一反应是使用可用的工具从外部来源获取更多信息。 22 | > 但是,工具不限于只读操作。例如,您可以使用它们来更新数据库。在该工具返回附加上下文后,将对包含新获得的信息对 LLM 进行另一次调用。 23 | > LLM 可以选择返回给用户最终生成的答案,或者它可以决定需要通过其可用工具执行更多的操作。 24 | > 对于每一个输出(Observation)之后会紧跟着一个思考(Thought),思考下一步做什么,如果发现任务全部完成就输出最终答案。 25 | ```json 26 | // 导入 https://ongdb-contrib.github.io/graphene/app/ 查看可视化效果 27 | {"data":{"nodes":[{"x":235,"y":448,"color":"#80d0d6","label":"用户","properties":[{"id":"a2938fae","key":"id","type":"ID","defaultValue":"","limitMin":"","limitMax":"","isRequired":true,"isAutoGenerated":true,"isSystem":true,"description":""}],"id":"a8a09d86","isSelected":false,"isNode":true},{"x":521,"y":189,"color":"#E16F23","label":"大语言模型","properties":[{"id":"a2903785","key":"id","type":"ID","defaultValue":"","limitMin":"","limitMax":"","isRequired":true,"isAutoGenerated":true,"isSystem":true,"description":""}],"id":"ad9a5489","isSelected":false,"isNode":true},{"x":522,"y":686,"color":"#E16F23","label":"大语言模型","properties":[{"id":"aba6f9a3","key":"id","type":"ID","defaultValue":"","limitMin":"","limitMax":"","isRequired":true,"isAutoGenerated":true,"isSystem":true,"description":""}],"id":"a0834ab1","isSelected":false,"isNode":true},{"x":810,"y":440,"color":"#76cb84","label":"工具","properties":[{"id":"a1a4da97","key":"id","type":"ID","defaultValue":"","limitMin":"","limitMax":"","isRequired":true,"isAutoGenerated":true,"isSystem":true,"description":""}],"id":"a5863091","isSelected":false,"isNode":true},{"x":524.1188049316406,"y":444.8241882324219,"color":"#bca48f","label":"代理","properties":[{"id":"ac97d889","key":"id","type":"ID","defaultValue":"","limitMin":"","limitMax":"","isRequired":true,"isAutoGenerated":true,"isSystem":true,"description":""}],"id":"aca7f28a","isSelected":false,"isNode":true},{"x":476,"y":745,"color":"#8C8C8C","label":"LangChain代理流程","properties":[{"id":"a4adb68d","key":"id","type":"ID","defaultValue":"","limitMin":"","limitMax":"","isRequired":true,"isAutoGenerated":true,"isSystem":true,"description":""}],"id":"a0b111a6","isSelected":false,"isNode":true}],"edges":[{"startNodeId":"a8a09d86","endNodeId":"aca7f28a","middlePointOffset":[32.55940246582031,-53.58790588378906],"properties":[{"id":"afa4bf89","key":"id","type":"ID","defaultValue":"","limitMin":"","limitMax":"","isRequired":true,"isAutoGenerated":true,"isSystem":true,"description":""}],"label":"1.提出问题","id":"a4b76b9a","isSelected":false,"isEdge":true,"color":"#666666","pathStrokeDasharray":"none"},{"startNodeId":"aca7f28a","endNodeId":"a8a09d86","middlePointOffset":[24.559402465820312,56.41209411621094],"properties":[{"id":"a6a968bc","key":"id","type":"ID","defaultValue":"","limitMin":"","limitMax":"","isRequired":true,"isAutoGenerated":true,"isSystem":true,"description":""}],"label":"响应","id":"ac93bcb3","isSelected":false,"isEdge":true,"color":"#5795bc","pathStrokeDasharray":"none"},{"startNodeId":"aca7f28a","endNodeId":"a5863091","middlePointOffset":[6.0594024658203125,65.41209411621094],"properties":[{"id":"aaa624ad","key":"id","type":"ID","defaultValue":"","limitMin":"","limitMax":"","isRequired":true,"isAutoGenerated":true,"isSystem":true,"description":""}],"label":"4.使用可用工具检索其他信息","id":"af88a1a3","isSelected":false,"isEdge":true,"color":"#666666","pathStrokeDasharray":"none"},{"startNodeId":"a5863091","endNodeId":"aca7f28a","middlePointOffset":[1.0594024658203125,-47.58790588378906],"properties":[{"id":"af88fcbd","key":"id","type":"ID","defaultValue":"","limitMin":"","limitMax":"","isRequired":true,"isAutoGenerated":true,"isSystem":true,"description":""}],"label":"5.返回使用工具检索到的信息","id":"a99cbda7","isSelected":false,"isEdge":true,"color":"#5795bc","pathStrokeDasharray":"none"},{"startNodeId":"aca7f28a","endNodeId":"a0834ab1","middlePointOffset":[-72.94059753417969,13.412094116210938],"properties":[{"id":"a990feb7","key":"id","type":"ID","defaultValue":"","limitMin":"","limitMax":"","isRequired":true,"isAutoGenerated":true,"isSystem":true,"description":""}],"label":"6.发送其他上下文","id":"aab68592","isSelected":false,"isEdge":true,"color":"#666666","pathStrokeDasharray":"none"},{"startNodeId":"a0834ab1","endNodeId":"aca7f28a","middlePointOffset":[73.05940246582031,16.412094116210938],"properties":[{"id":"aa9c839a","key":"id","type":"ID","defaultValue":"","limitMin":"","limitMax":"","isRequired":true,"isAutoGenerated":true,"isSystem":true,"description":""}],"label":"7.返回最终的回答","id":"a086a5b4","isSelected":false,"isEdge":true,"color":"#5795bc","pathStrokeDasharray":"none"},{"startNodeId":"ad9a5489","endNodeId":"aca7f28a","middlePointOffset":[-83.44059753417969,14.912094116210938],"properties":[{"id":"afa2a1b2","key":"id","type":"ID","defaultValue":"","limitMin":"","limitMax":"","isRequired":true,"isAutoGenerated":true,"isSystem":true,"description":""}],"label":"3.请求代理的说明和可选的使用工具","id":"a6ad1ea9","isSelected":false,"isEdge":true,"color":"#5795bc","pathStrokeDasharray":"none"},{"startNodeId":"aca7f28a","endNodeId":"ad9a5489","middlePointOffset":[74.55940246582031,9.912094116210938],"properties":[{"id":"a8a6e884","key":"id","type":"ID","defaultValue":"","limitMin":"","limitMax":"","isRequired":true,"isAutoGenerated":true,"isSystem":true,"description":""}],"label":"2.输入问题以及代理提示","id":"af9f07a0","isSelected":false,"isEdge":true,"color":"#666666","pathStrokeDasharray":"none"}]}} 28 | ``` 29 | ![chains-flow](images/chains-flow.png) 30 | 31 | ## 安装与启动步骤 32 | 33 | 1. 在`Langchain2ONgDB`项目根目录下创建`.env`文件,并设置在`env.example`文件中展示的系统运行依赖的参数。 34 | 35 | 2. 并确保已经在本地安装了 [ONgDB](https://github.com/graphfoundation/ongdb) ,并构建了数据集。 36 | 37 | 3. 启动后端:运行`backend/src/main.py`。 38 | 39 | 4. 启动前端:在`frontend`目录下运行以下命令。 40 | 41 | ```shell 42 | npm install 43 | npm run-script build 44 | npm start 45 | ``` 46 | 47 | 6. 浏览器访问:`http://localhost:3000`。 48 | 49 | ## 样例问答 50 | ``` 51 | # 山西都有哪些上市公司? 52 | MATCH p0=(n0:股票)-[r0:地域]->(n1:地域) WHERE n1.value='山西' 53 | RETURN DISTINCT n0 AS n4 LIMIT 10; 54 | 55 | # 建筑工程行业有多少家上市公司? 56 | MATCH p0=(n0:股票)-[r0:所属行业]->(n1:行业) 57 | WHERE n1.value='建筑工程' 58 | RETURN COUNT(DISTINCT n0) AS n4; 59 | 60 | # 火力发电行业博士学历的男性高管有多少位? 61 | MATCH 62 | p0=(n1:行业)<-[r0:所属行业]-(n0:股票)<-[r1:任职于]-(n2:高管)-[r2:性别]->(n3:性别)-[r4:别名]->(n5:性别_别名), 63 | p1=(n2)-[r3:学历]->(n4:学历) 64 | WHERE n1.value='火力发电' AND n5.value='男性' AND n4.value='博士' 65 | RETURN COUNT(DISTINCT n2) AS n3; 66 | 67 | # 在山东由硕士学历的男性高管任职的上市公司,都属于哪些行业? 68 | MATCH 69 | p1=(n1:`地域`)<-[:`地域`]-(n2:`股票`)<-[:`任职于`]-(n3:`高管`)-[:`性别`]->(n4:`性别`), 70 | p2=(n3)-[:`学历`]->(n5:学历), 71 | p3=(n2)-[:`所属行业`]->(n6:行业) 72 | WHERE n1.value='山东' AND n5.value='硕士' AND n4.value='M' 73 | RETURN DISTINCT n6.value AS hy; 74 | 75 | # 2023年三月六日上市的股票有哪些? 76 | MATCH p0=(n0:股票)-[r0:上市日期]->(n1:上市日期) 77 | WHERE (n1.value>=20230306 AND n1.value<=20230306) 78 | RETURN DISTINCT n0 AS n4 LIMIT 10; 79 | 80 | # 刘卫国是哪个公司的高管? 81 | MATCH p0=(n0:股票)<-[r0:任职于]-(n1:高管) 82 | WHERE n1.value='刘卫国' 83 | RETURN DISTINCT n0 AS n4 LIMIT 10; 84 | ``` 85 | 86 | ## 样例问题 87 | ``` 88 | 刘卫国是哪个公司的高管? 89 | 海南有哪些上市公司? 90 | 在北京由硕士学历的女性高管任职的上市公司,都属于哪些行业? 91 | 李建国在哪些公司任职? 92 | 2023年三月六日上市的股票有哪些? 93 | 建筑工程行业有多少家上市公司? 94 | 水泥行业博士学历的男性高管有多少位? 95 | #刘卫国是哪个公司的高管,这些公司的高管是谁? 96 | #刘卫国是哪个公司的高管,这些公司都在什么地方? 97 | ``` 98 | 99 | ## 相关依赖包 100 | ```shell 101 | pip install langchain==0.0.150 102 | pip install openai==0.27.4 103 | pip install neo4j-driver==1.7.6 104 | ``` 105 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Python base image with the desired version 2 | FROM python:3.11 3 | 4 | # Set the working directory 5 | WORKDIR /app 6 | 7 | COPY p* /app 8 | 9 | # Install Poetry 10 | RUN pip install poetry==1.4.2 11 | 12 | # Install project dependencies using Poetry 13 | RUN poetry install 14 | 15 | # Copy the project files into the container 16 | COPY ./src /app 17 | 18 | # Expose any necessary ports 19 | EXPOSE 7860 20 | 21 | # Start the application 22 | CMD ["poetry", "run", "python", "main.py"] -------------------------------------------------------------------------------- /backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "langchain2neo4j" 3 | version = "0.1.0" 4 | description = "Integrating Neo4j database into langchain ecosystem" 5 | authors = ["Ibis Prevedello ", "Tomaz Bratanic "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.11" 10 | pandas = "^2.0.0" 11 | langchain = "^0.0.136" 12 | neo4j = "^5.7.0" 13 | fastapi = "^0.95.0" 14 | rich = "^13.3.3" 15 | openai = "^0.27.4" 16 | google-search-results = "^2.4.2" 17 | uvicorn = "^0.21.1" 18 | setuptools = "^67.6.1" 19 | tiktoken = "^0.3.3" 20 | 21 | [tool.poetry.group.dev.dependencies] 22 | black = "^23.1.0" 23 | ipykernel = "^6.21.2" 24 | mypy = "^1.1.1" 25 | ruff = "^0.0.254" 26 | httpx = "^0.23.3" 27 | pytest = "^7.2.2" 28 | types-requests = "^2.28.11" 29 | requests = "^2.28.0" 30 | 31 | [build-system] 32 | requires = ["poetry-core"] 33 | build-backend = "poetry.core.masonry.api" 34 | -------------------------------------------------------------------------------- /backend/src/agent.py: -------------------------------------------------------------------------------- 1 | from langchain.agents.agent import AgentExecutor 2 | from langchain.agents.tools import Tool 3 | from langchain.chat_models import ChatOpenAI 4 | from langchain.memory import ConversationBufferMemory, ReadOnlySharedMemory 5 | from langchain.agents import initialize_agent 6 | from langchain.agents import AgentType 7 | 8 | from backend.src.env import getEnv 9 | from cypher_tool import LLMCypherGraphChain 10 | 11 | 12 | class GraphAgent(AgentExecutor): 13 | """Graph agent""" 14 | 15 | @staticmethod 16 | def function_name(): 17 | return "GraphAgent" 18 | 19 | @classmethod 20 | def initialize(cls, graph, model_name, *args, **kwargs): 21 | if model_name in ['gpt-3.5-turbo', 'gpt-4']: 22 | llm = ChatOpenAI(temperature=0, model_name=model_name, openai_api_key=getEnv('OPENAI_KEY')) 23 | else: 24 | raise Exception(f"Model {model_name} is currently not supported") 25 | 26 | memory = ConversationBufferMemory( 27 | memory_key="chat_history", return_messages=True) 28 | readonlymemory = ReadOnlySharedMemory(memory=memory) 29 | 30 | cypher_tool = LLMCypherGraphChain( 31 | llm=llm, graph=graph, verbose=True, memory=readonlymemory) 32 | 33 | # Load the tool configs that are needed. 34 | tools = [ 35 | Tool( 36 | name="Cypher search", 37 | func=cypher_tool.run, 38 | description=""" 39 | 利用此工具在股票、高管数据库中搜索信息,该数据库专门用于回答与股票和高管相关的问题。 40 | 这个专用的工具提供了简化的搜索功能,可帮助您轻松找到所需的股票和高管信息。 41 | 输入应该是一个完整的问题。 42 | """, 43 | ) 44 | ] 45 | 46 | agent_chain = initialize_agent( 47 | tools, llm, agent=AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION, verbose=True, memory=memory) 48 | 49 | return agent_chain 50 | 51 | def __init__(self, *args, **kwargs): 52 | super().__init__(*args, **kwargs) 53 | 54 | def run(self, *args, **kwargs): 55 | return super().run(*args, **kwargs) 56 | -------------------------------------------------------------------------------- /backend/src/cypher_tool.py: -------------------------------------------------------------------------------- 1 | from backend.src.env import getEnv 2 | from database import Neo4jDatabase 3 | from pydantic import BaseModel, Extra 4 | from langchain.prompts.base import BasePromptTemplate 5 | from langchain.chains.llm import LLMChain 6 | from langchain.chains.base import Chain 7 | from langchain.memory import ReadOnlySharedMemory, ConversationBufferMemory 8 | from langchain.prompts import ( 9 | ChatPromptTemplate, 10 | SystemMessagePromptTemplate, 11 | HumanMessagePromptTemplate, 12 | ) 13 | from typing import Dict, List, Any 14 | 15 | from logger import logger 16 | 17 | examples = """ 18 | # 山西都有哪些上市公司? 19 | MATCH p0=(n0:股票)-[r0:地域]->(n1:地域) WHERE n1.value='山西' 20 | RETURN DISTINCT n0 AS n4 LIMIT 10; 21 | 22 | # 建筑工程行业有多少家上市公司? 23 | MATCH p0=(n0:股票)-[r0:所属行业]->(n1:行业) 24 | WHERE n1.value='建筑工程' 25 | RETURN COUNT(DISTINCT n0) AS n4; 26 | 27 | # 火力发电行业博士学历的男性高管有多少位? 28 | MATCH 29 | p0=(n1:行业)<-[r0:所属行业]-(n0:股票)<-[r1:任职于]-(n2:高管)-[r2:性别]->(n3:性别)-[r4:别名]->(n5:性别_别名), 30 | p1=(n2)-[r3:学历]->(n4:学历) 31 | WHERE n1.value='火力发电' AND n5.value='男性' AND n4.value='博士' 32 | RETURN COUNT(DISTINCT n2) AS n3; 33 | 34 | # 在山东由硕士学历的男性高管任职的上市公司,都属于哪些行业? 35 | MATCH 36 | p1=(n1:`地域`)<-[:`地域`]-(n2:`股票`)<-[:`任职于`]-(n3:`高管`)-[:`性别`]->(n4:`性别`), 37 | p2=(n3)-[:`学历`]->(n5:学历), 38 | p3=(n2)-[:`所属行业`]->(n6:行业) 39 | WHERE n1.value='山东' AND n5.value='硕士' AND n4.value='M' 40 | RETURN DISTINCT n6.value AS hy; 41 | 42 | # 2023年三月六日上市的股票有哪些? 43 | MATCH p0=(n0:股票)-[r0:上市日期]->(n1:上市日期) 44 | WHERE (n1.value>=20230306 AND n1.value<=20230306) 45 | RETURN DISTINCT n0 AS n4 LIMIT 10; 46 | 47 | # 刘卫国是哪个公司的高管? 48 | MATCH p0=(n0:股票)<-[r0:任职于]-(n1:高管) 49 | WHERE n1.value='刘卫国' 50 | RETURN DISTINCT n0 AS n4 LIMIT 10; 51 | """ 52 | 53 | SYSTEM_TEMPLATE = """ 54 | 您是一名助手,能够根据示例Cypher查询生成Cypher查询。 55 | 示例Cypher查询是:\n""" + examples + """\n 56 | 不要回复除Cypher查询以外的任何解释或任何其他信息。 57 | 您永远不要为你的不准确回复感到抱歉,并严格根据提供的Cypher示例生成Cypher语句。 58 | 不要提供任何无法从密码示例中推断出的Cypher语句。 59 | """ 60 | 61 | SYSTEM_CYPHER_PROMPT = SystemMessagePromptTemplate.from_template(SYSTEM_TEMPLATE) 62 | 63 | HUMAN_TEMPLATE = "{question}" 64 | HUMAN_PROMPT = HumanMessagePromptTemplate.from_template(HUMAN_TEMPLATE) 65 | 66 | 67 | class LLMCypherGraphChain(Chain, BaseModel): 68 | """Chain that interprets a prompt and executes python code to do math. 69 | """ 70 | 71 | llm: Any 72 | """LLM wrapper to use.""" 73 | system_prompt: BasePromptTemplate = SYSTEM_CYPHER_PROMPT 74 | human_prompt: BasePromptTemplate = HUMAN_PROMPT 75 | input_key: str = "question" #: :meta private: 76 | output_key: str = "answer" #: :meta private: 77 | graph: Neo4jDatabase 78 | memory: ReadOnlySharedMemory 79 | 80 | class Config: 81 | """Configuration for this pydantic object.""" 82 | 83 | extra = Extra.forbid 84 | arbitrary_types_allowed = True 85 | 86 | @property 87 | def input_keys(self) -> List[str]: 88 | """Expect input key. 89 | :meta private: 90 | """ 91 | return [self.input_key] 92 | 93 | @property 94 | def output_keys(self) -> List[str]: 95 | """Expect output key. 96 | :meta private: 97 | """ 98 | return [self.output_key] 99 | 100 | def _call(self, inputs: Dict[str, str]) -> Dict[str, str]: 101 | logger.debug(f"Cypher generator inputs: {inputs}") 102 | chat_prompt = ChatPromptTemplate.from_messages( 103 | [self.system_prompt] + inputs['chat_history'] + [self.human_prompt] 104 | ) 105 | cypher_executor = LLMChain( 106 | prompt=chat_prompt, llm=self.llm, callback_manager=self.callback_manager 107 | ) 108 | cypher_statement = cypher_executor.predict( 109 | question=inputs[self.input_key], stop=["Output:"]) 110 | self.callback_manager.on_text( 111 | "Generated Cypher statement:", color="green", end="\n", verbose=self.verbose 112 | ) 113 | self.callback_manager.on_text( 114 | cypher_statement, color="blue", end="\n", verbose=self.verbose 115 | ) 116 | # If Cypher statement was not generated due to lack of context 117 | if not "MATCH" in cypher_statement: 118 | return {'answer': 'Missing context to create a Cypher statement'} 119 | context = self.graph.query(cypher_statement) 120 | logger.debug(f"Cypher generator context: {context}") 121 | 122 | return {'answer': context} 123 | 124 | 125 | if __name__ == "__main__": 126 | from langchain.chat_models import ChatOpenAI 127 | 128 | llm = ChatOpenAI( 129 | openai_api_key=getEnv('OPENAI_KEY'), 130 | temperature=0.3) 131 | database = Neo4jDatabase(host="bolt://localhost:7687", 132 | user="ongdb", password="123456") 133 | 134 | memory = ConversationBufferMemory( 135 | memory_key="chat_history", return_messages=True) 136 | readonlymemory = ReadOnlySharedMemory(memory=memory) 137 | 138 | chain = LLMCypherGraphChain(llm=llm, verbose=True, graph=database, memory=readonlymemory) 139 | 140 | output = chain.run( 141 | # "海南有哪些上市公司?" 142 | "在北京由硕士学历的女性高管任职的上市公司,都属于哪些行业?" 143 | ) 144 | 145 | print(output) 146 | -------------------------------------------------------------------------------- /backend/src/database.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Dict 2 | from neo4j import GraphDatabase 3 | from logger import logger 4 | 5 | 6 | class Neo4jDatabase: 7 | def __init__(self, host: str = "neo4j://localhost:7687", 8 | user: str = "ongdb", 9 | password: str = "123456"): 10 | """Initialize the movie database""" 11 | 12 | self.driver = GraphDatabase.driver(host, auth=(user, password)) 13 | 14 | def query( 15 | self, 16 | cypher_query: str, 17 | params: Optional[Dict] = {} 18 | ) -> List[Dict[str, str]]: 19 | logger.debug(cypher_query) 20 | with self.driver.session() as session: 21 | result = session.run(cypher_query, params) 22 | # Limit to at most 50 results 23 | return [r.values()[0] for r in result][:50] 24 | 25 | 26 | if __name__ == "__main__": 27 | database = Neo4jDatabase(host="bolt://localhost:7687", 28 | user="ongdb", password="123456") 29 | 30 | a = database.query(""" 31 | MATCH (n) RETURN {count: count(*)} AS count 32 | """) 33 | 34 | print(a) 35 | -------------------------------------------------------------------------------- /backend/src/endpoints.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from agent import GraphAgent 4 | from backend.src.env import getEnv 5 | from database import Neo4jDatabase 6 | from fastapi import APIRouter, HTTPException, Query 7 | from run import get_result_and_thought_using_graph 8 | 9 | neo4j_host = getEnv("NEO4J_URL") 10 | neo4j_user = getEnv("NEO4J_USER") 11 | neo4j_password = getEnv("NEO4J_PASS") 12 | model_name = getEnv("MODEL_NAME") 13 | # build router 14 | router = APIRouter() 15 | logger = logging.getLogger(__name__) 16 | graph = Neo4jDatabase( 17 | host=neo4j_host, user=neo4j_user, password=neo4j_password) 18 | 19 | # 不使用记忆组件功能,每次接口初始化 20 | # agent_graph = GraphAgent.initialize( 21 | # graph=graph, model_name=model_name) 22 | 23 | 24 | @router.get("/predict") 25 | def get_load(message: str = Query(...)): 26 | try: 27 | agent_graph = GraphAgent.initialize( 28 | graph=graph, model_name=model_name) 29 | return get_result_and_thought_using_graph(agent_graph, message) 30 | except Exception as e: 31 | # Log stack trace 32 | logger.exception(e) 33 | raise HTTPException(status_code=500, detail=str(e)) from e 34 | 35 | 36 | # # ============================FAKE DATA============================ 37 | # @router.get("/predict") 38 | # def get_load(message: str = Query(...)): 39 | # try: 40 | # 41 | # return fake_map[message] 42 | # except Exception as e: 43 | # # Log stack trace 44 | # logger.exception(e) 45 | # raise HTTPException(status_code=500, detail=str(e)) from e 46 | # 47 | # 48 | # fake_map = { 49 | # "刘卫国是哪个公司的高管?": 50 | # { 51 | # "response": "刘卫国是 600148.SH 和 000030.SZ 公司的高管。", 52 | # "thought": "> Entering new AgentExecutor chain...\n{\n \"action\": \"Cypher search\",\n \"action_input\": \"刘卫国是哪个公司的高管?\"\n}\n\n> Entering new LLMCypherGraphChain chain...\nGenerated Cypher statement:\nMATCH p0=(n0:股票)<-[r0:任职于]-(n1:高管) \nWHERE n1.value='刘卫国'\nRETURN DISTINCT n0 AS n4 LIMIT 10;\n\n> Finished chain.\n\nObservation: [, ]\nThought:{\n \"action\": \"Final Answer\",\n \"action_input\": \"刘卫国是 600148.SH 和 000030.SZ 公司的高管。\"\n}\n\n> Finished chain." 53 | # }, 54 | # "海南有哪些上市公司?": 55 | # { 56 | # "response": "海南的上市公司有:600259.SH、000955.SZ、000505.SZ、300189.SZ、600238.SH、600221.SH、000886.SZ、000566.SZ、600759.SH、600515.SH。", 57 | # "thought": "> Entering new AgentExecutor chain...\n{\n \"action\": \"Cypher search\",\n \"action_input\": \"海南上市公司\"\n}\n\n> Entering new LLMCypherGraphChain chain...\nGenerated Cypher statement:\nMATCH p0=(n0:股票)-[r0:地域]->(n1:地域) WHERE n1.value='海南' \nRETURN DISTINCT n0 AS n4 LIMIT 10;\n\n> Finished chain.\n\nObservation: [, , , , , , , , , ]\nThought:{\n \"action\": \"Final Answer\",\n \"action_input\": \"海南的上市公司有:600259.SH、000955.SZ、000505.SZ、300189.SZ、600238.SH、600221.SH、000886.SZ、000566.SZ、600759.SH、600515.SH。\"\n}\n\n> Finished chain." 58 | # }, 59 | # "在北京由硕士学历的女性高管任职的上市公司,都属于哪些行业?": 60 | # { 61 | # "response": "在北京由硕士学历的女性高管任职的上市公司,属于家用电器、广告包装、化纤、建筑工程、家居用品、文教休闲、供气供热、运输设备、超市连锁、化工原料、汽车配件、中成药、通信设备、煤炭开采、装修装饰、百货、啤酒、生物制药、综合类、软件服务、区域地产、园区开发、全国地产、电信运营、IT设备、电气设备、小金属、酒店餐饮、普钢、饲料、汽车整车、铅锌、专用机械、新型电力、化学制药、航空、医疗保健、半导体、银行、矿物制品、服饰、证券、渔业、元器件、船舶、仓储物流、种植业、农业综合、水力发电、多元金融行业。", 62 | # "thought": "> Entering new AgentExecutor chain...\n{\n \"action\": \"Cypher search\",\n \"action_input\": \"在北京由硕士学历的女性高管任职的上市公司,都属于哪些行业?\"\n}\n\n> Entering new LLMCypherGraphChain chain...\nGenerated Cypher statement:\nMATCH \n p1=(n1:`地域`)<-[:`地域`]-(n2:`股票`)<-[:`任职于`]-(n3:`高管`)-[:`性别`]->(n4:`性别`),\n p2=(n3)-[:`学历`]->(n5:学历),\n p3=(n2)-[:`所属行业`]->(n6:行业)\nWHERE n1.value='北京' AND n5.value='硕士' AND n4.value='F'\nRETURN DISTINCT n6.value AS hy;\n\n> Finished chain.\n\nObservation: ['家用电器', '广告包装', '化纤', '建筑工程', '家居用品', '文教休闲', '供气供热', '运输设备', '超市连锁', '化工原料', '汽车配件', '中成药', '通信设备', '煤炭开采', '装修装饰', '百货', '啤酒', '生物制药', '综合类', '软件服务', '区域地产', '园区开发', '全国地产', '电信运营', 'IT设备', '电气设备', '小金属', '酒店餐饮', '普钢', '饲料', '汽车整车', '铅锌', '专用机械', '新型电力', '化学制药', '航空', '医疗保健', '半导体', '银行', '矿物制品', '服饰', '证券', '渔业', '元器件', '船舶', '仓储物流', '种植业', '农业综合', '水力发电', '多元金融']\nThought:{\n \"action\": \"Final Answer\",\n \"action_input\": \"在北京由硕士学历的女性高管任职的上市公司,属于家用电器、广告包装、化纤、建筑工程、家居用品、文教休闲、供气供热、运输设备、超市连锁、化工原料、汽车配件、中成药、通信设备、煤炭开采、装修装饰、百货、啤酒、生物制药、综合类、软件服务、区域地产、园区开发、全国地产、电信运营、IT设备、电气设备、小金属、酒店餐饮、普钢、饲料、汽车整车、铅锌、专用机械、新型电力、化学制药、航空、医疗保健、半导体、银行、矿物制品、服饰、证券、渔业、元器件、船舶、仓储物流、种植业、农业综合、水力发电、多元金融行业。\"\n}\n\n> Finished chain." 63 | # }, 64 | # "李建国在哪些公司任职?": 65 | # { 66 | # "response": "根据查询结果,李建国曾在002065.SZ、002929.SZ、600123.SH、601825.SH、300369.SZ等公司任职。", 67 | # "thought": "> Entering new AgentExecutor chain...\n{\n \"action\": \"Cypher search\",\n \"action_input\": \"李建国 在哪些公司任职?\"\n}\n\n> Entering new LLMCypherGraphChain chain...\nGenerated Cypher statement:\nMATCH p0=(n0:股票)<-[r0:任职于]-(n1:高管) \nWHERE n1.value='李建国'\nRETURN DISTINCT n0 AS n4;\n\n> Finished chain.\n\nObservation: [, , , , , , , ]\nThought:{\n \"action\": \"Final Answer\",\n \"action_input\": \"根据查询结果,李建国曾在002065.SZ、002929.SZ、600123.SH、601825.SH、300369.SZ等公司任职。\"\n}\n\n> Finished chain." 68 | # }, 69 | # "2023年三月六日上市的股票有哪些?": 70 | # { 71 | # "response": "301322.SZ", 72 | # "thought": "> Entering new AgentExecutor chain...\n{\n \"action\": \"Cypher search\",\n \"action_input\": \"2023年三月六日上市的股票有哪些?\"\n}\n\n> Entering new LLMCypherGraphChain chain...\nGenerated Cypher statement:\nMATCH p0=(n0:股票)-[r0:上市日期]->(n1:上市日期) \nWHERE (n1.value>=20230306 AND n1.value<=20230306) \nRETURN DISTINCT n0 AS n4;\n\n> Finished chain.\n\nObservation: []\nThought:{\n \"action\": \"Final Answer\",\n \"action_input\": \"301322.SZ\"\n}\n\n> Finished chain." 73 | # }, 74 | # "建筑工程行业有多少家上市公司?": 75 | # { 76 | # "response": "建筑工程行业共有上市公司115家。", 77 | # "thought": "> Entering new AgentExecutor chain...\n{\n \"action\": \"Cypher search\",\n \"action_input\": \"建筑工程行业上市公司数量\"\n}\n\n> Entering new LLMCypherGraphChain chain...\nGenerated Cypher statement:\nMATCH p0=(n0:股票)-[r0:所属行业]->(n1:行业) \nWHERE n1.value='建筑工程'\nRETURN COUNT(DISTINCT n0) AS n4;\n\n> Finished chain.\n\nObservation: [115]\nThought:{\n \"action\": \"Final Answer\",\n \"action_input\": \"建筑工程行业共有上市公司115家。\"\n}\n\n> Finished chain." 78 | # }, 79 | # "水泥行业博士学历的男性高管有多少位?": 80 | # { 81 | # "response": "水泥行业有46名具有博士学位的男性高管。", 82 | # "thought": "> Entering new AgentExecutor chain...\n{\n \"action\": \"Cypher search\",\n \"action_input\": \"水泥行业博士学历的男性高管有多少位?\"\n}\n\n> Entering new LLMCypherGraphChain chain...\nGenerated Cypher statement:\nMATCH \n p0=(n1:行业)<-[r0:所属行业]-(n0:股票)<-[r1:任职于]-(n2:高管)-[r2:性别]->(n3:性别)-[r4:别名]->(n5:性别_别名),\n p1=(n2)-[r3:学历]->(n4:学历) \nWHERE n1.value='水泥' AND n5.value='男性' AND n4.value='博士'\nRETURN COUNT(DISTINCT n2) AS n3;\n\n> Finished chain.\n\nObservation: [46]\nThought:{\n \"action\": \"Final Answer\",\n \"action_input\": \"水泥行业有46名具有博士学位的男性高管。\"\n}\n\n> Finished chain." 83 | # } 84 | # } 85 | 86 | -------------------------------------------------------------------------------- /backend/src/env.py: -------------------------------------------------------------------------------- 1 | env_dict = {} 2 | 3 | with open('../../.env') as file: 4 | for line in file: 5 | # Strip leading and trailing whitespaces and split by '=' 6 | key, value = line.strip().split('=') 7 | # Add the key-value pair to the dictionary 8 | env_dict[key] = value 9 | 10 | 11 | def getEnv(key): 12 | return env_dict[key] 13 | 14 | 15 | if __name__ == '__main__': 16 | print(getEnv('OPENAI_KEY')) 17 | -------------------------------------------------------------------------------- /backend/src/logger.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | log_format = "%(asctime)s - %(levelname)s - %(message)s" 5 | log_level_value = os.environ.get("LOG_LEVEL", logging.INFO) 6 | 7 | logging.basicConfig( 8 | level=log_level_value, 9 | format=log_format, 10 | ) 11 | 12 | logger = logging.getLogger("imdb") 13 | -------------------------------------------------------------------------------- /backend/src/main.py: -------------------------------------------------------------------------------- 1 | from endpoints import router as endpoints_router 2 | from fastapi import FastAPI 3 | from fastapi.middleware.cors import CORSMiddleware 4 | 5 | 6 | def create_app(): 7 | """Create the FastAPI app and include the router.""" 8 | app = FastAPI() 9 | 10 | origins = [ 11 | "*", 12 | ] 13 | 14 | app.add_middleware( 15 | CORSMiddleware, 16 | allow_origins=origins, 17 | allow_credentials=True, 18 | allow_methods=["*"], 19 | allow_headers=["*"], 20 | ) 21 | 22 | app.include_router(endpoints_router) 23 | return app 24 | 25 | 26 | app = create_app() 27 | 28 | if __name__ == "__main__": 29 | import uvicorn 30 | 31 | uvicorn.run(app, host="0.0.0.0", port=7860) 32 | -------------------------------------------------------------------------------- /backend/src/run.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import io 3 | 4 | from logger import logger 5 | 6 | 7 | def get_result_and_thought_using_graph( 8 | langchain_object, 9 | message: str, 10 | ): 11 | """Get result and thought from extracted json""" 12 | try: 13 | if hasattr(langchain_object, "verbose"): 14 | langchain_object.verbose = True 15 | chat_input = None 16 | memory_key = "" 17 | if hasattr(langchain_object, "memory") and langchain_object.memory is not None: 18 | memory_key = langchain_object.memory.memory_key 19 | 20 | for key in langchain_object.input_keys: 21 | if key not in [memory_key, "chat_history"]: 22 | chat_input = {key: message} 23 | 24 | with io.StringIO() as output_buffer, contextlib.redirect_stdout(output_buffer): 25 | try: 26 | output = langchain_object(chat_input) 27 | except ValueError as exc: 28 | # make the error message more informative 29 | logger.debug(f"Error: {str(exc)}") 30 | output = langchain_object.run(chat_input) 31 | thought = output_buffer.getvalue().strip() 32 | logger.info(thought) 33 | 34 | except Exception as exc: 35 | raise ValueError(f"Error: {str(exc)}") from exc 36 | 37 | return {"response": output["output"], "thought": thought} 38 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | frontend: 4 | build: 5 | context: frontend/. 6 | hostname: frontend 7 | restart: always 8 | container_name: frontend 9 | ports: 10 | - 3000:3000 11 | depends_on: 12 | - backend 13 | links: 14 | - backend 15 | backend: 16 | build: 17 | context: backend/. 18 | hostname: backend 19 | restart: always 20 | container_name: backend 21 | ports: 22 | - 7860:7860 23 | env_file: 24 | - .env 25 | -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Node.js image as the base 2 | FROM node:19-alpine 3 | 4 | # Set the working directory 5 | WORKDIR /app 6 | 7 | # Serve the built files using a lightweight HTTP server 8 | RUN npm install -g serve 9 | 10 | # Copy the package.json and package-lock.json files to the working directory 11 | COPY package*.json ./ 12 | 13 | # Install dependencies 14 | RUN npm install 15 | 16 | # Copy the rest of the application code to the working directory 17 | COPY . . 18 | 19 | # Build the production version of the React app 20 | RUN npm run build 21 | 22 | # Expose the port on which the app will run 23 | EXPOSE 3000 24 | 25 | # Start the app 26 | CMD ["serve", "-s", "build"] -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "movies-chat", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.10.6", 7 | "@emotion/styled": "^11.10.6", 8 | "@fortawesome/react-fontawesome": "^0.2.0", 9 | "@testing-library/jest-dom": "^5.16.5", 10 | "@testing-library/react": "^13.4.0", 11 | "@testing-library/user-event": "^13.5.0", 12 | "ansi-to-html": "^0.7.2", 13 | "axios": "^1.3.5", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-icons": "^4.8.0", 17 | "react-scripts": "5.0.1", 18 | "socket.io-client": "^4.6.1", 19 | "web-vitals": "^2.1.4" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ongdb-contrib/langchain2ongdb/4ec9a3854e74ac689fb5ce2cdd80887e53a804b4/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ongdb-contrib/langchain2ongdb/4ec9a3854e74ac689fb5ce2cdd80887e53a804b4/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ongdb-contrib/langchain2ongdb/4ec9a3854e74ac689fb5ce2cdd80887e53a804b4/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | background-color: #4b0082; 9 | } 10 | 11 | .App { 12 | text-align: center; 13 | } 14 | 15 | .chat-container { 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | justify-content: center; 20 | height: 100%; 21 | } 22 | 23 | .chat-box { 24 | width: 80%; 25 | max-width: 1200px; 26 | background-color: white; 27 | border-radius: 10px; 28 | padding: 20px; 29 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 30 | } 31 | 32 | .message { 33 | display: flex; 34 | justify-content: flex-end; 35 | } 36 | 37 | .message-bot { 38 | text-align: left; 39 | background-color: #e0e0e0; 40 | border-radius: 10px; 41 | padding: 5px 10px; 42 | margin-bottom: 5px; 43 | max-width: 75%; /* Set a maximum width */ 44 | word-wrap: break-word; /* Enable word wrapping */ 45 | } 46 | 47 | .message-user { 48 | text-align: right; 49 | background-color: #4b0082; 50 | color: white; 51 | border-radius: 10px; 52 | padding: 5px 10px; 53 | margin-bottom: 5px; 54 | max-width: 75%; /* Set a maximum width */ 55 | word-wrap: break-word; /* Enable word wrapping */ 56 | } 57 | 58 | .message-bot-container, 59 | .message-user-container { 60 | display: flex; 61 | width: 100%; 62 | } 63 | 64 | .message-bot-container { 65 | justify-content: flex-start; 66 | } 67 | 68 | .message-user-container { 69 | justify-content: flex-end; 70 | } 71 | 72 | 73 | 74 | 75 | .input-container { 76 | display: flex; 77 | } 78 | 79 | .input { 80 | flex-grow: 1; 81 | border: 1px solid #ddd; 82 | border-radius: 4px; 83 | padding: 6px; 84 | } 85 | 86 | .send-button { 87 | background-color: #4b0082; 88 | color: white; 89 | border: none; 90 | border-radius: 4px; 91 | padding: 6px 12px; 92 | margin-left: 6px; 93 | cursor: pointer; 94 | } 95 | 96 | .send-button[disabled] { 97 | cursor: default; 98 | } 99 | 100 | .message { 101 | margin-bottom: 10px; 102 | } 103 | 104 | .user { 105 | font-weight: bold; 106 | color: #4b0082; 107 | } 108 | 109 | .message-content { 110 | display: flex; 111 | flex-direction: column; 112 | align-items: center; 113 | } 114 | 115 | .movie-cards-container { 116 | display: flex; 117 | overflow-x: auto; 118 | gap: 10px; 119 | justify-content: center; 120 | } 121 | 122 | .movie-card { 123 | display: flex; 124 | flex-direction: column; 125 | background-color: white; 126 | border-radius: 10px; 127 | padding: 10px; 128 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 129 | min-width: 200px; 130 | max-width: 300px; 131 | } 132 | 133 | .movie-top-section { 134 | display: flex; 135 | flex-direction: row; 136 | align-items: flex-start; 137 | margin-bottom: 10px; 138 | } 139 | 140 | .movie-card h4 { 141 | font-size: 18px; 142 | margin: 5px 0; 143 | } 144 | 145 | .movie-card p { 146 | font-size: 14px; 147 | margin: 5px 0; 148 | } 149 | 150 | .movie-cover-container { 151 | flex: 0 0 auto; 152 | margin-right: 10px; 153 | } 154 | 155 | .movie-details { 156 | flex: 1 1 auto; 157 | display: flex; 158 | flex-direction: column; 159 | } 160 | 161 | .movie-cover { 162 | max-height: 100%; 163 | max-width: 100px; 164 | border-radius: 10px; 165 | object-fit: cover; 166 | margin-bottom: 10px; 167 | } 168 | 169 | .watch-now-button { 170 | background-color: indigo; 171 | color: white; 172 | border: none; 173 | border-radius: 5px; 174 | padding: 5px 10px; 175 | font-size: 14px; 176 | cursor: pointer; 177 | margin-top: auto; 178 | } 179 | 180 | .synopsis { 181 | font-size: 14px; 182 | margin: 10px 0; 183 | text-align: justify; 184 | } 185 | 186 | .genre { 187 | font-size: 14px; 188 | margin: 10px 0; 189 | text-align: center; 190 | } 191 | 192 | .toggle-thought-button { 193 | color: indigo; 194 | cursor: pointer; 195 | border: 0px; 196 | background-color: transparent; 197 | text-align: center; 198 | font-size: 1rem; 199 | } 200 | 201 | .thought-modal { 202 | position: fixed; 203 | top: 0; 204 | left: 0; 205 | right: 0; 206 | bottom: 0; 207 | background-color: rgba(0, 0, 0, 0.5); 208 | display: flex; 209 | align-items: center; 210 | justify-content: center; 211 | } 212 | 213 | .thought-modal-content { 214 | background-color: white; 215 | padding: 20px; 216 | border-radius: 5px; 217 | max-width: 80%; 218 | width: 800px; 219 | text-align: left; 220 | } 221 | 222 | .thought-icon { 223 | margin-right: 0.5rem; 224 | font-size: 1.2rem; 225 | } 226 | 227 | .close-modal-button { 228 | background-color: indigo; 229 | color: white; 230 | border: none; 231 | border-radius: 5px; 232 | padding: 5px 10px; 233 | cursor: pointer; 234 | display: block; 235 | margin: 0 auto; 236 | } -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { BsFillGearFill } from "react-icons/bs" 3 | import axios from 'axios'; 4 | import './App.css'; 5 | var Convert = require('ansi-to-html'); 6 | var convert = new Convert({newline: true}); 7 | 8 | 9 | 10 | 11 | function App() { 12 | const [input, setInput] = useState(''); 13 | const [messages, setMessages] = useState([]); 14 | const [loading, setLoading] = useState(false); 15 | const messagesEndRef = useRef(null); 16 | const [currentThought, setCurrentThought] = useState(null); 17 | const [thought, setThought] = useState(null); 18 | const [isThoughtVisible, setIsThoughtVisible] = useState(false); 19 | 20 | 21 | const scrollToBottom = () => { 22 | messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); 23 | }; 24 | 25 | useEffect(scrollToBottom, [messages]); 26 | 27 | const sendMessage = async () => { 28 | if (input.trim() === '') return; 29 | 30 | setMessages([...messages, { text: input, user: 'You' }]); 31 | setInput(''); 32 | 33 | try { 34 | setLoading(true); 35 | const response = await axios.get("http://127.0.0.1:7860/predict", { 36 | params: { message: input }, 37 | }); 38 | 39 | const answer = response.data.response; 40 | const movies = response.data.movies; 41 | const thought = response.data.thought; 42 | 43 | setThought(thought); 44 | setIsThoughtVisible(false); 45 | 46 | setMessages((prevMessages) => [ 47 | ...prevMessages, 48 | { 49 | text: answer, 50 | user: "Agent", 51 | movies: movies, 52 | thought: thought, 53 | }, 54 | ]); 55 | } catch (error) { 56 | console.error("Error fetching message:", error); 57 | } finally { 58 | setLoading(false); 59 | } 60 | }; 61 | 62 | const ThoughtModal = ({ show, onClose, thought }) => { 63 | if (!show) { 64 | return null; 65 | } 66 | 67 | return ( 68 |
69 |
70 | {/*

Bot's Thought

*/} 71 |
72 | 75 |
76 |
77 | ); 78 | 79 | }; 80 | 81 | 82 | const MovieCard = ({ movie }) => { 83 | const { title, year, directors, Overview, Poster, genre } = movie; 84 | 85 | return ( 86 |
87 |
88 |
89 | {title} 90 |
91 |
92 |

{title}

93 |

{year}

94 |

{directors.join(', ')}

95 |
96 |
97 |

{genre.join(', ')}

98 |

{Overview}

99 | 100 |
101 | ); 102 | }; 103 | 104 | 105 | const handleKeyPress = (event) => { 106 | if (event.key === 'Enter') { 107 | sendMessage(); 108 | } 109 | }; 110 | 111 | return ( 112 |
113 | setCurrentThought(null)} 116 | thought={currentThought} 117 | /> 118 |
119 |
120 |
121 | {messages.map((message, index) => ( 122 | 123 |
130 |
135 | 136 | {message.text} 137 | {message.user === "Agent" && message.thought && ( 138 | 144 | )} 145 |
146 | 147 |
148 | 149 | {message.movies && message.movies.length > 0 && ( 150 |
151 | {message.movies.map((movie) => ( 152 | 153 | ))} 154 |
155 | )} 156 |
157 | ))} 158 |
159 |
160 |
161 | setInput(e.target.value)} 166 | onKeyPress={handleKeyPress} 167 | disabled={loading} 168 | /> 169 | 172 |
173 |
174 |
175 |
176 | ); 177 | 178 | 179 | } 180 | 181 | export default App; 182 | -------------------------------------------------------------------------------- /frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /images/chains-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ongdb-contrib/langchain2ongdb/4ec9a3854e74ac689fb5ce2cdd80887e53a804b4/images/chains-flow.png -------------------------------------------------------------------------------- /images/qa-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ongdb-contrib/langchain2ongdb/4ec9a3854e74ac689fb5ce2cdd80887e53a804b4/images/qa-1.png -------------------------------------------------------------------------------- /images/qa-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ongdb-contrib/langchain2ongdb/4ec9a3854e74ac689fb5ce2cdd80887e53a804b4/images/qa-2.png --------------------------------------------------------------------------------