This commit is contained in:
Fromsko 2024-01-04 22:10:24 +08:00
parent d65c238668
commit 6fe00eded6
56 changed files with 13900 additions and 41 deletions

43
.gitignore vendored
View File

@ -160,37 +160,12 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# ---> Go
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
# ---> Vue
# gitignore template for Vue.js projects
#
# Recommended template: Node.gitignore
# TODO: where does this rule come from?
docs/_book
# TODO: where does this rule come from?
test/
./frontend/**node_modules**
**/frontend/**static**
**/old/**
**/web-tom/**
**/res/gen/**
**/res/upload/**
*/*.yaml
*/*.spec
*/*.log

View File

@ -1,3 +1,67 @@
# kmap
# **知识图谱-后端**
基于ChatGPT + Vue 构建的知识图谱
<div align="center">
**🌍基于知识图谱的软件功能点提取与分析**
🛠️ 简化数据获取流程(后端接口)
---
</div>
## 📑 功能特点
- 上传 软件文档 `type`: `[ docx | doc | txt ]`
- 对接 `LLM` 并采用 `prompt` 进行微调
---
- 提供简单的 FastAPI 接口
- 用户操作: `/api/v1/user`
- 用户登录
- `POST`
- 用户注册
- `PUT`
- 更新用户
- `/api/v1/user/update`
- `POST`
- 删除用户
- `DELETE`
## 📦 安装
首先,确保您的 `Python` 版本为 **3.9** 或更高。然后,执行以下步骤来安装项目:
**通用安装:**
```bash
pip install -r requirements.txt
```
## 🚀 快速开始
1. 填写配置文件 `res/config.json`
```json
{
"OPEN_KEY": ""
}
```
2. 启动应用程序 `main.py`
```bash
cd KnowledgeMap
python main.py
```
3. 访问项目文档:
- 打开浏览器访问 <http://localhost:8000/docs> 查看接口文档
- 访问 `http://host:port` 查看前端显示
## 🔗 示例请求
暂时还没写完

83
api/__init__.py Normal file
View File

@ -0,0 +1,83 @@
# -*- encoding: utf-8 -*-
"""
@Desc : 基础路由📦
"""
import aiohttp
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import HTMLResponse
from api.api_auth import router as auth_router
from api.api_upload import Config, router as upload_router
# Mount Path
STATIC = Config.get_path("frontend", "static")
# Router 处理
Base_api = FastAPI(redoc_url=None, title="知识图谱分析(后端)", version="2.0")
Base_api.include_router(auth_router)
Base_api.include_router(upload_router)
@Base_api.get("/ping", tags=["测试"])
async def ping(request: Request):
"""
### 测试程序是否正常运行
用于测试程序是否正常运行的简单路由
**返回**:
- `dict`: 包含应答信息的字典格式示例:
```json
{
"ping": "pong",
"client_ip": "客户端的IP地址"
}
```
"""
return {
"ping": "pong",
"client_ip": request.client.host,
}
async def fetch_google_with_proxy(proxy_url: str):
url = "https://www.google.com"
async with aiohttp.ClientSession() as session:
async with session.get(url, proxy=proxy_url) as response:
return await response.text()
@Base_api.get("/google", tags=["测试"])
async def access_google_with_proxy(proxy_url: str):
"""
测试访问谷歌, 验证代理连通性
Args:
`proxy_url`: 代理地址
Returns:
状态
"""
try:
result = await fetch_google_with_proxy(proxy_url)
return {"status": "success", "content": result}
except Exception as e:
return {"status": "error", "error_message": str(e)}
@Base_api.get("/home", response_class=HTMLResponse)
async def read_home():
try:
index_path = str(STATIC.joinpath("index.html"))
# 读取index.html文件内容
with open(index_path, "r", encoding="utf-8") as file:
content = file.read()
return HTMLResponse(content=content)
except FileNotFoundError:
# 如果文件不存在抛出HTTP异常
raise HTTPException(status_code=404, detail="Index.html not found")
@Base_api.get("/", tags=["测试"])
async def read_root():
return {"message": "Hello, World!"}

107
api/api_auth.py Normal file
View File

@ -0,0 +1,107 @@
# -*- encoding: utf-8 -*-
"""
@Desc : 登录鉴权
"""
from fastapi import APIRouter, Form
from utils.auth import AuthParams
from utils.config import Config, hash_password
router = APIRouter(prefix="/api/v1")
auth_params = AuthParams(Config.get_select_config("mysql"))
@router.put("/user", tags=["用户信息"])
async def add_user(username: str = Form(...), password: str = Form(...)):
"""
**添加用户路由**
Args:
- `params` (Form): 表单
- `username`: 用户名
- `password`: 密码
Returns:
- `json`: 包含添加操作的结果和用户信息的字典
"""
# 查询用户是否存在
query_result = auth_params.query(username)
if query_result['msg'] == auth_params.Query.notfound:
hashed_password = hash_password(password)
return auth_params.insert(username, hashed_password)
return query_result
@router.post("/user", tags=["用户信息"])
async def user_login(username: str = Form(...), password: str = Form(...)):
"""
**用户登录路由**
Args:
- `params` (Form): 表单
- `username`: 用户名
- `password`: 密码
Returns:
- `json`: 包含用户登录操作的结果和用户信息的字典
"""
# 用户不存在
query_result = auth_params.query(username)
if query_result['msg'] == auth_params.Query.notfound:
return query_result
# 检查密码是否匹配
stored_password = query_result["msg"][2]
if hash_password(password) == stored_password:
auth_params.base_msg["msg"] = "登录成功"
else:
auth_params.base_msg["msg"] = "登录失败"
return auth_params.base_msg
@router.get("/user/{username}", tags=["用户信息"])
async def get_user(username: str):
"""
**查询用户路由**
Args:
- `params` username: 要查询的用户的用户名
Returns:
- `json`: 包含查询操作的结果和用户信息的字典
"""
return auth_params.query(username)
@router.post("/user/update", tags=["用户信息"])
async def update_user(username: str = Form(...), password: str = Form(...)):
"""
**更新用户数据**
Args:
- `params` (Form): 表单
- `username`: 用户名
- `password`: 密码
Returns:
- `json`: 包含查询操作的结果和用户信息的字典
"""
return auth_params.update(username, hash_password(password))
@router.delete("/user/{username}", tags=["用户信息"])
async def delete_user(username: str):
"""
删除用户路由
Args:
- `username` (str): 要删除的用户的用户名
Returns:
- `json`: 包含删除操作的结果的字典
"""
# 查询用户是否存在
return auth_params.delete(username)

View File

@ -13,10 +13,9 @@ import openai
from openai import OpenAI
from utils.config import Config, log
from utils.model.data import ChatModel
class BaseClientModel:
class BaseClientModel_:
def __init__(self, chat_model: ChatModel):
self.client: OpenAI = OpenAI(

View File

@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
# @File : api_upload.py
# @Description : 文件上传 📦
import tiktoken
from fastapi import APIRouter, File, UploadFile
from typing_extensions import Optional, Union
from utils.agents.executor import SummarizeRobot
from utils.common import BaseClient
from utils.config import Config, log
from utils.model.adapter import Doc, DocumentAdapter, Docx, Txt
from utils.model.agent import ChatModel, TaskModel
from utils.model.error import UploadAPIError
router = APIRouter(prefix="/api/v1")
def parser_upload_type(file: UploadFile) -> Union[str, dict]:
"""解析上传文件
参数:
- `file`: 上传文件
返回:
- `str | None`: 结果
"""
document: Optional[DocumentAdapter]
if (file_type := file.filename.split('.')[-1]) not in ["docx", "doc", "txt"]:
return UploadAPIError.type_upload_error
try:
if file_type == "docx":
document = DocumentAdapter(Docx())
elif file_type == "doc":
document = DocumentAdapter(Doc())
else:
document = DocumentAdapter(Txt())
except (IOError, Exception):
return UploadAPIError.parser_upload_error
else:
return document.parser(file)
def statistical_token_len(text: str, size: int = 2000) -> Union[int, dict]:
"""统计查询一次需要的 token 数量
参数:
- `text`: 文本信息
返回:
- `int`: token 数量
"""
num_tokens: int = 0
try:
encoding = tiktoken.encoding_for_model("gpt-3.5-turbo")
num_tokens = len(encoding.encode(text))
except (Exception, RuntimeError):
log.warn(f"统计 Tokens 错误: {num_tokens}")
return num_tokens
if num_tokens <= size:
log.info(f"预计消耗 Tokens: {num_tokens}")
return num_tokens
return UploadAPIError.size_upload_error
@router.post("/upload", tags=["文件上传"])
async def upload_file(file: UploadFile = File(...)):
"""文档上传
参数:
- `file`: 文件(支持格式为: docx | doc | txt)
返回:
- `json`: 解析参数
"""
if file.filename is None: # 文件不存在
return UploadAPIError.empty_upload_error
if isinstance((content := parser_upload_type(file)), dict):
return content # 格式错误
if isinstance((tokens := statistical_token_len(content)), dict):
return tokens # 超出大小
# TODO: 采用插件系统 | 代理模式 | 装饰器
chat: BaseClient = BaseClient(
ChatModel(**Config.get_select_config("chat"))
)
robot = SummarizeRobot()
task = TaskModel(
agent=robot.robot(chat),
description=content,
)
reply = await task.execute()
log.info(reply)
ddc = robot.complete_data(reply[task.agent.role]['result'])
log.info(ddc)
return ddc
#
# reply = await chat.send_to_chatgpt(content)
# log.info(
# reply
# )
# return reply

23
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

24
frontend/README.md Normal file
View File

@ -0,0 +1,24 @@
# bs
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

5
frontend/babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

19
frontend/jsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

10665
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
frontend/package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "bs",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build --dest ./static"
},
"dependencies": {
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^1.0.2",
"animate.css": "^4.1.1",
"axios": "^1.4.0",
"core-js": "^3.8.3",
"echarts": "^5.4.3",
"element-ui": "^2.15.13",
"moment": "^2.29.4",
"three": "^0.157.0",
"vue": "^2.6.14",
"vue-router": "^3.5.1",
"vuex": "^3.6.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"css-loader": "^6.7.3",
"file-loader": "^6.2.0",
"less": "^4.1.3",
"less-loader": "^11.1.0",
"sass": "^1.32.7",
"sass-loader": "^12.6.0",
"style-loader": "^3.3.2",
"vue-template-compiler": "^2.6.14"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

BIN
frontend/public/fa.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>基于软件功能点提示知识图谱网站</title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

24
frontend/src/App.vue Normal file
View File

@ -0,0 +1,24 @@
<template>
<div>
<router-view />
</div>
</template>
<script>
import Header from '../src/components/Header/Header.vue'
import SideMenu from '../src/views/SideMenu/SideMenu.vue'
export default {
components: { Header, SideMenu },
name: 'App',
}
</script>
<style>
body {
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 0px;
margin: 0px;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
frontend/src/assets/a.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,29 @@
<template>
<div>
1
</div>
</template>
<script>
import router from '@/router'
export default {
data () {
return {
}
},
created(){
},
mounted() {
},
methods: {
},
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,47 @@
<template>
<div>
<el-menu
:default-active="$route.path"
class="el-menu-demo"
mode="horizontal"
@select="handleSelect"
background-color="#545c64"
:router="true"
text-color="#fff"
active-text-color="white">
<el-menu-item index="/home" style="margin-left: 50px"
>图谱分析</el-menu-item
>
<div style="display: flex; padding-left: 100px">
<el-menu-item index="/Login" style="margin-left: 600px"
>退出登录</el-menu-item
>
</div>
</el-menu>
</div>
</template>
<script>
import router from '@/router'
export default {
data() {
return {
activeIndex: '1',
options: [],
value: '',
}
},
methods: {
handleSelect(key, keyPath) {
console.log(key, keyPath)
},
change(e) {
console.log(e)
this.$router.push({ name: 'Content', query: { id: e } })
this.$router.go(0)
},
},
}
</script>
<style scoped lang="less"></style>

13
frontend/src/main.js Normal file
View File

@ -0,0 +1,13 @@
import Vue from 'vue'
import App from './App.vue'
import ElementUI from 'element-ui'
import router from './router/index'
import 'element-ui/lib/theme-chalk/index.css';
import axios from 'axios';
Vue.config.productionTip = false
Vue.use(ElementUI)
Vue.prototype.$axios=axios;
new Vue({
router,
render: h => h(App),
}).$mount('#app')

View File

@ -0,0 +1,54 @@
import Vue from "vue";
import Router from 'vue-router'
import Login from '../views/Login/Login.vue'
import Home from '../views/home/home.vue'
import Header from '../components/Header/Header.vue'
import Body from '../components/Body/Body.vue'
import Range from '../views/Range/Range.vue'
import Content_1 from '../views/Content_1/Content_1.vue'
import Register from '../views/Register/Register.vue'
import NetworkGraph from '../views/NetworkGraph/NetworkGraph.vue'
Vue.use(Router)
export default new Router({
mode: 'hash',
routes: [{
path: '/',
name: 'Login',
component: Login
},{
path: '/NetworkGraph',
name: 'NetworkGraph',
component: NetworkGraph
},{
path: '/Home',
name: 'Home',
component: Home
},{
path: '/Login',
name: 'Login',
component: Login
},{
path: '/Body',
name: 'Body',
component: Body
},{
path: '/Range',
name: 'Range',
component: Range
},{
path: '/Register',
name: 'Register',
component: Register
},{
path: '/Content_1',
name: 'Content_1',
component: Content_1
},{
path: '/Header',
name: 'Header',
component: Header
}]
})

View File

@ -0,0 +1,12 @@
import Range from '../views/Range/Range.vue'
import Content_1 from '../views/Content_1/Content_1.vue'
const routes = [
// 其他路由规则...
{ path: '/Range', component: Range },
{ path: '/Content_1', component: Content_1 },
]
const router = new VueRouter({
routes,
})

View File

@ -0,0 +1,218 @@
<template>
<div>
<el-container>
<el-aside width="200px"><SideMenu /></el-aside>
<el-container>
<el-header>
<el-menu
:default-active="$route.path"
class="el-menu-demo"
mode="horizontal"
@select="handleSelect"
background-color="#545c64"
:router="true"
text-color="#fff"
active-text-color="white">
<el-menu-item index="/home" style="margin-left: 50px"
>图谱分析</el-menu-item
>
<div style="display: flex; padding-left: 100px">
<el-menu-item index="/Login" style="margin-left: 600px"
>退出登录</el-menu-item
>
</div>
</el-menu></el-header
>
<el-main style="height: 100vh">
<el-tag type="warning" style="margin-left: -1260px; margin-top: 20px">
文档介绍
</el-tag>
<el-card class="box-card">
<div slot="header" class="clearfix" v-if="a != ''">
{{ this.a }}
</div>
<div slot="header" class="clearfix" v-else>请分析文档后再查看</div>
</el-card>
<el-tag type="" style="margin-left: -1260px; margin-top: 30px">
内容分析列
</el-tag>
<el-collapse v-model="activeNames" @change="handleChange">
<el-collapse-item
title="通过知识图谱分析,您的文档设计为您提供系统总结:"
name="1">
<div style="font-weight: 600" v-if="sum != ''">
{{ sum }}
</div>
<div style="font-weight: 600" v-else>请分析文档后再查看</div>
</el-collapse-item>
<el-collapse-item
title="通过知识图谱分析,您的文档设计有部分不足:"
name="2">
<div style="font-weight: 600" v-if="sums != ''">
{{ sums }}
</div>
<div style="font-weight: 600" v-else>请分析文档后再查看</div>
</el-collapse-item>
</el-collapse>
<el-tag type="danger" style="margin-left: -1260px; margin-top: 20px">
表格分析栏
</el-tag>
<el-table :data="tableData" border style="width: 100%">
<el-table-column
prop="name"
label="功能"
width="200"></el-table-column>
<el-table-column label="功能内容" width="700">
<template slot-scope="scope">
<span v-if="scope.row.relation.relation">
{{ scope.row.relation.relation }}
</span>
<span v-else>系统暂时无法展示功能内容</span>
</template>
</el-table-column>
</el-table>
<el-tag type="warning" style="margin-left: -1260px; margin-top: 20px">
文档图谱分析
</el-tag>
<Net />
</el-main>
</el-container>
</el-container>
<!-- <Net /> -->
</div>
</template>
<script>
import Header from '../../components/Header/Header.vue'
import SideMenu from '../SideMenu/SideMenu.vue'
import Net from '../NetworkGraph/NetworkGraph.vue'
export default {
components: { Header, SideMenu, Net },
data() {
return {
tableData: [],
sum: '',
sums: '',
a: '',
}
},
computed: {
op: {},
},
mounted() {
const list = JSON.parse(localStorage.getItem('data')).origin.function
// const dataArray = Object.entries(list)
this.tableData = Object.entries(list).map(([name, relation]) => ({
name,
relation,
}))
this.sum = JSON.parse(localStorage.getItem('data')).origin.summarize
console.log(this.sum)
this.sums = JSON.parse(localStorage.getItem('data')).origin.new_summarize
this.a = JSON.parse(localStorage.getItem('data')).origin.summarize
},
methods: {},
}
</script>
<style lang="scss" scoped>
Page {
color: white;
height: 100vh;
}
::v-deep.el-header {
padding: 0;
}
::v-deep.el-main[data-v-1b439646] {
line-height: 30px;
}
::v-deep.el-table[data-v-1b439646] {
margin-top: 0px;
}
::v-deep.summary-section .summary[data-v-1b439646] {
height: 140px;
}
::v-deep.el-main {
padding: 0;
margin-left: -1px;
}
.el-header,
.el-footer {
background-color: #b3c0d1;
color: #333;
text-align: center;
line-height: 60px;
}
.el-aside {
background-color: #d3dce6;
color: #333;
text-align: center;
line-height: 200px;
}
.el-main {
background-color: #e9eef3;
color: #333;
text-align: center;
line-height: 160px;
}
body > .el-container {
margin-bottom: 40px;
}
.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
line-height: 260px;
}
.el-container:nth-child(7) .el-aside {
line-height: 320px;
}
body {
margin: 0;
padding: 0;
}
// Main dashboard container
.dashboard {
display: flex;
flex-direction: column;
}
// Page header styling
.page-header {
font-weight: 300;
font-size: 20px;
margin-bottom: 20px;
}
// Table styling
.el-table {
margin-top: 20px;
}
// Summary section styling
.summary-section {
display: flex;
justify-content: space-around;
margin-top: 20px;
// Each summary block styling
.summary {
margin-left: 200px;
height: 400px;
font-weight: 600;
display: flex;
flex-direction: column;
align-items: center;
// Summary value styling
.summary-value {
font-weight: 500;
}
}
}
</style>

View File

@ -0,0 +1,135 @@
<template>
<div class="login_container">
<el-row>
<el-col :span="22" :xs="0"></el-col>
</el-row>
<el-col :span="12" :xs="24">
<el-form class="login_form" ref="form" :model="form" :rules="rules">
<h1>Hello</h1>
<h2>欢迎登录知识图谱分析数据网站</h2>
<el-form-item prop="name">
<el-input v-model="form.name" prefix-icon="el-icon-user"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="form.password"
prefix-icon="el-icon-lock"
type="password"
show-password></el-input>
</el-form-item>
<el-form-item>
<el-button
class="login_button"
type="primary"
size="default"
@click="login(form)">
登录
</el-button>
</el-form-item>
<el-form-item>
<el-button
class="login_button"
type="primary"
size="default"
@click="login_1()">
注册
</el-button>
</el-form-item>
</el-form>
</el-col>
</div>
</template>
<script>
import router from '@/router'
export default {
data() {
return {
form: {
name: '',
password: '',
},
rules: {
name: [{ required: true, message: '请输入账号', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
},
}
},
methods: {
login_1() {
router.push('/Register')
},
login(form) {
if (this.form.password === '' || this.form.name === '') {
this.info()
} else {
const formData = new FormData()
formData.append('username', this.form.name)
formData.append('password', this.form.password)
this.$axios.post('/api/v1/user', formData).then((res) => {
if (res.data.msg === '登录失败') {
this.warning()
} else {
this.success()
localStorage.setItem('user', this.form.name)
router.push('/Range')
}
console.log(res)
})
}
},
warning() {
this.$message({
message: '输入的密码或者账号错误',
type: 'warning',
})
},
info() {
this.$message({
message: '请输入密码或者账号',
})
},
success() {
this.$message({
message: '登录成功!欢迎回来',
type: 'success',
})
},
},
}
</script>
<style lang="scss" scoped>
.login_container {
width: 100%;
height: 100vh;
background: url('https://pic1.zhimg.com/v2-66f210d04b273c6616f00bece467223a_1440w.jpg?source=172ae18b')
no-repeat;
background-size: cover;
.login_form {
animation-name: bounceIn;
animation-duration: 3s;
background-color: darkgrey;
border-radius: 2%;
opacity: 0.9;
position: relative;
width: 80%;
top: 30vh;
background-size: cover;
padding: 10px 10px 40px 10px;
margin-top: -100px;
margin-left: 800px;
h1 {
color: white;
font-size: 40px;
}
h2 {
font-size: 20px;
color: white;
margin: 20px 0px;
}
.login_button {
width: 100%;
}
}
}
</style>

View File

@ -0,0 +1,152 @@
<template>
<div>
<div
class="graph-container"
ref="ecGraph"
style="width: 100%; height: 70vh; margin-top: 10px"></div>
</div>
</template>
<script>
import * as echarts from 'echarts'
export default {
data() {
return {
ecGraph: null,
a: '',
tableData: '',
s: '',
}
},
mounted() {
this.initGraph()
},
created() {
this.tableData = JSON.parse(localStorage.getItem('data')).origin.function
const good = {
name: '功能点',
itemStyle: {
color: 'rgb(140,69,40)',
},
}
const array = JSON.parse(localStorage.getItem('data')).clean[0]
array.push(good)
this.s = array
console.log(this.s)
},
methods: {
initGraph() {
this.ecGraph = echarts.init(this.$refs.ecGraph)
const graphOption = {
series: [
{
type: 'graph',
layout: 'force',
symbolSize: 70,
animation: true,
roam: true,
label: {
show: true,
},
edgeSymbol: ['none', 'arrow'],
edgeSymbolSize: 10,
force: {
initLayout: 'circular',
repulsion: 400,
gravity: 0.1,
edgeLength: 200,
},
data: this.s,
links: [
{
source: this.s.length - 1,
target: 0,
},
{
source: this.s.length - 1,
target: 1,
},
{
source: this.s.length - 1,
target: 2,
},
{
source: this.s.length - 1,
target: 3,
},
{
source: this.s.length - 1,
target: 4,
},
{
source: this.s.length - 1,
target: 5,
},
{
source: this.s.length - 1,
target: 6,
},
{
source: this.s.length - 1,
target: 7,
},
{
source: this.s.length - 1,
target: 8,
},
{
source: this.s.length - 1,
target: 9,
},
{
source: this.s.length - 1,
target: 10,
},
{
source: this.s.length - 1,
target: 11,
},
{
source: this.s.length - 1,
target: 12,
},
{
source: this.s.length - 1,
target: 13,
},
{
source: this.s.length - 1,
target: 14,
},
{
source: this.s.length - 1,
target: 15,
},
{
source: this.s.length - 1,
target: 16,
},
{
source: this.s.length - 1,
target: 17,
},
{
source: this.s.length - 1,
target: 18,
},
],
},
],
}
this.ecGraph.setOption(graphOption)
},
},
}
</script>
<style lang="scss" scoped>
.graph-container {
width: 100%;
}
</style>

View File

@ -0,0 +1,213 @@
<template>
<div>
<el-container>
<el-aside width="200px"><SideMenu /></el-aside>
<el-container>
<el-header>
<el-menu
:default-active="$route.path"
class="el-menu-demo"
mode="horizontal"
@select="handleSelect"
background-color="#545c64"
:router="true"
text-color="#fff"
active-text-color="white"
>
<el-menu-item index="/home" style="margin-left: 50px"
>图谱分析</el-menu-item
>
<div style="display: flex; padding-left: 100px">
<el-menu-item index="/Login" style="margin-left: 600px"
>退出登录</el-menu-item
>
</div>
</el-menu></el-header
>
<el-main>
<div>
<el-carousel indicator-position="outside" height="400px">
<el-carousel-item v-for="item in list" :key="item">
<img :src="item.img" style="width: 100%; height: 400px" />
</el-carousel-item>
</el-carousel>
</div>
<div>
<div style="font-weight: 700; font-size: 20px">
为了帮助用户更好的分析文档本网站设有知识图谱功能只需要点击下方上传文件即可
</div>
<template>
<div style="height: 100vh">
<!-- 拖拽上传 -->
<el-upload
class="upload-demo"
drag
action="/api/v1/upload"
:before-upload="beforeUpload"
:on-success="handleSuccess"
:on-error="handleError"
limit="1"
multiple
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
</div>
<div class="el-upload__tip" slot="tip">
<strong>只能上传 doc | docx | txt 文件</strong>
</div>
</el-upload>
<!--
<el-upload
action="/api/v1/upload"
:before-upload="beforeUpload"
:on-success="handleSuccess"
:on-error="handleError"
>
<el-button
type="primary"
v-loading.fullscreen.lock="fullscreenLoading"
@click="openFullScreen1"
>点击分析文档上传</el-button
>
</el-upload> -->
</div>
</template>
</div>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
import router from '@/router'
import Header from '../../components/Header/Header.vue'
import SideMenu from '../SideMenu/SideMenu.vue'
export default {
components: { Header, SideMenu },
data() {
return {
fileList: [],
fullscreenLoading: false,
list: [
{
img: 'https://img0.baidu.com/it/u=2370989968,2150449142&fm=253&fmt=auto&app=138&f=JPEG?w=939&h=500',
},
{
img: 'https://img1.baidu.com/it/u=1563923172,2779050201&fm=253&fmt=auto&app=120&f=JPEG?w=640&h=360',
},
{
img: 'https://img2.baidu.com/it/u=653212776,213855603&fm=253&fmt=auto&app=138&f=JPEG?w=830&h=500',
},
],
}
},
mounted() {},
methods: {
beforeUpload(file) {
//
console.log(file)
},
handleSuccess(response, file) {
//
if (response.clean !== '') {
this.fullscreenLoading = false
console.log(response)
if (response.err === '文件格式错误') {
this.warning()
} else if (response.err === '提取失败, 请查看日志!') {
this.warning_1()
} else {
this.fullscreenLoading = false
localStorage.setItem('data', JSON.stringify(response))
router.push('/Content_1')
}
}
},
handleError(error, file) {
//
console.error(error)
},
warning() {
this.$message({
message: '上传的文件格式错误',
type: 'warning',
})
},
warning_1() {
this.$message({
message: '服务器繁忙,请等待几分钟',
type: 'warning',
})
},
openFullScreen1() {
this.fullscreenLoading = true
},
},
}
</script>
<style lang="scss" scoped>
::v-deep.el-header {
padding: 0;
}
::v-deep.el-main {
flex: none;
}
::v-deep.el-main[data-v-1e94e001] {
line-height: 50px;
}
::v-deep.el-main {
padding: 0;
margin-left: -1px;
}
.el-header,
.el-footer {
background-color: #b3c0d1;
color: #333;
text-align: center;
line-height: 60px;
}
.el-aside {
background-color: #d3dce6;
color: #333;
text-align: center;
line-height: 200px;
}
.el-main {
background-color: #e9eef3;
color: #333;
text-align: center;
line-height: 160px;
}
body > .el-container {
margin-bottom: 40px;
}
.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
line-height: 260px;
}
.el-container:nth-child(7) .el-aside {
line-height: 320px;
}
.el-carousel__item h3 {
color: #475669;
font-size: 18px;
opacity: 0.75;
line-height: 10px;
margin: 0;
}
.el-carousel__item:nth-child(2n) {
background-color: #99a9bf;
}
.el-carousel__item:nth-child(2n + 1) {
background-color: #d3dce6;
}
</style>

View File

@ -0,0 +1,150 @@
<template>
<div class="login_container">
<el-row>
<el-col :span="22" :xs="0"></el-col>
</el-row>
<el-col :span="12" :xs="24">
<el-form class="login_form" ref="form" :model="form" :rules="rules">
<h1>Hello</h1>
<h2>欢迎注册知识图谱分析数据网站</h2>
<el-form-item prop="name">
<el-input
v-model="form.name"
prefix-icon="el-icon-user"
placeholder="请输入注册账号"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="form.password"
placeholder="请输入注册密码"
prefix-icon="el-icon-lock"
type="password"
show-password></el-input>
</el-form-item>
<el-form-item prop="repassword">
<el-input
v-model="form.repassword"
placeholder="请再次输入注册密码"
prefix-icon="el-icon-lock"
type="password"
show-password></el-input>
</el-form-item>
<el-form-item> </el-form-item>
<el-form-item>
<el-button
class="login_button"
type="primary"
size="default"
@click="login(form)">
注册
</el-button>
</el-form-item>
</el-form>
</el-col>
</div>
</template>
<script>
import router from '@/router'
export default {
data() {
return {
form: {
name: '',
password: '',
repassword: '',
},
rules: {
name: [{ required: true, message: '请输入注册账号', trigger: 'blur' }],
password: [
{ required: true, message: '请输入注册密码', trigger: 'blur' },
],
repassword: [
{ required: true, message: '请输入注册密码', trigger: 'blur' },
],
},
}
},
methods: {
login(form) {
if (this.form.password === '' || this.form.name === '') {
this.info()
} else if (this.form.password !== this.form.repassword) {
this.warning()
} else {
const formData = new FormData()
formData.append('username', this.form.name)
formData.append('password', this.form.password)
this.$axios.put('/api/v1/user', formData).then((res) => {
if (res.data.msg !== '添加成功') {
this.warning_1()
} else {
this.success()
localStorage.setItem('user', 1)
router.push('/Range')
}
console.log(res)
})
}
},
warning() {
this.$message({
message: '输入的密码与第二次输入的密码不一致',
type: 'warning',
})
},
warning_1() {
this.$message({
message: '该账号已经被注册!',
type: 'warning',
})
},
info() {
this.$message({
message: '请输入注册密码或者账号',
})
},
success() {
this.$message({
message: '注册成功!欢迎回来',
type: 'success',
})
},
},
}
</script>
<style lang="scss" scoped>
.login_container {
width: 100%;
height: 100vh;
background: url('https://pic1.zhimg.com/v2-66f210d04b273c6616f00bece467223a_1440w.jpg?source=172ae18b')
no-repeat;
background-size: cover;
.login_form {
animation-name: bounceIn;
animation-duration: 3s;
background-color: darkgrey;
border-radius: 2%;
opacity: 0.9;
position: relative;
width: 80%;
top: 30vh;
background-size: cover;
padding: 10px 10px 40px 10px;
margin-top: -100px;
margin-left: 800px;
h1 {
color: white;
font-size: 40px;
}
h2 {
font-size: 20px;
color: white;
margin: 20px 0px;
}
.login_button {
width: 100%;
}
}
}
</style>

View File

@ -0,0 +1,68 @@
<template>
<div>
<el-container style="height: 1500px; border: 1px solid #eee">
<el-aside width="200px" style="background-color: rgb(238, 241, 246)">
<el-menu router>
<el-submenu>
<template slot="title"
><i class="el-icon-message"></i>功能点</template
>
<el-menu-item-group>
<el-menu-item index="/Range">上传word</el-menu-item>
<el-menu-item index="/Content_1">图谱分析</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</el-aside>
</el-container>
</div>
</template>
<script>
export default {
data() {
const item = {
date: '2016-05-02',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄',
}
return {
tableData: Array(10).fill(item),
img: [
{
imgs: 'https://p2.itc.cn/images01/20210621/4998136c64fc45baa7c11b38b05165b7.jpeg',
},
{
imgs: 'https://img.best73.com/20210324/zip_1616583234t3CRS6.jpg',
},
{
imgs: 'https://nimg.ws.126.net/?url=http%3A%2F%2Fdingyue.ws.126.net%2F2021%2F0726%2F4d7d1f46j00qwugbb000vc000hs008lg.jpg&thumbnail=660x2147483647&quality=80&type=jpg',
},
{
imgs: 'https://n.sinaimg.cn/spider2021612/155/w650h305/20210612/5751-krhvrxu0196238.jpg',
},
],
labelPosition: 'right',
}
},
created() {},
mounted() {},
methods: {},
}
</script>
<style lang="scss" scoped>
.el-header {
background-color: #b3c0d1;
color: #333;
line-height: 60px;
}
.el-aside {
color: #333;
}
::v-deep.el-table {
line-height: 0;
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<div>
<el-container>
<el-aside width="200px"><SideMenu /></el-aside>
<el-container>
<el-header><Header /></el-header>
<el-main>
<router-view />
</el-main>
<el-footer>Footer</el-footer>
</el-container>
</el-container>
</div>
</template>
<script>
import Header from '../../components/Header/Header.vue'
import SideMenu from '../SideMenu/SideMenu.vue'
export default {
components: { Header, SideMenu },
data() {
return {}
},
mounted() {},
methods: {},
}
</script>
<style lang="scss" scoped>
::v-deep.el-header {
padding: 0;
}
::v-deep.el-main {
flex: none;
}
::v-deep.el-main {
padding: 0;
margin-left: -1px;
}
.el-header,
.el-footer {
background-color: #b3c0d1;
color: #333;
text-align: center;
line-height: 60px;
}
.el-aside {
background-color: #d3dce6;
color: #333;
text-align: center;
line-height: 200px;
}
.el-main {
background-color: #e9eef3;
color: #333;
text-align: center;
line-height: 160px;
}
body > .el-container {
margin-bottom: 40px;
}
.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
line-height: 260px;
}
.el-container:nth-child(7) .el-aside {
line-height: 320px;
}
</style>

21
frontend/vue.config.js Normal file
View File

@ -0,0 +1,21 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
publicPath: '/static',
lintOnSave: false,
devServer: {
host: '0.0.0.0', //可以忽略不写
port: 8084,//它是用来修改你打开后的端口号的
open: true,//值为 true的话项目启动时自动打开到浏览器里边 false不会打开
proxy: {
'/api': {
target: 'http://127.0.0.1:8090',//跨域请求的公共地址
ws: false, //也可以忽略不写,不写不会影响跨域
changeOrigin: true, //是否开启跨域,值为 true 就是开启, false 不开启
// pathRewrite: {
// '^/api': ''//注册全局路径, 但是在你请求的时候前面需要加上 /api
// }
}
}
},
})

195
main.py Normal file
View File

@ -0,0 +1,195 @@
# # -*- encoding: utf-8 -*-
# # @File : main
# # @Docs : 程序入口
import multiprocessing
from typing import Optional
from pydantic import BaseModel
from starlette.staticfiles import StaticFiles
from uvicorn import Config, Server
from api import Base_api, STATIC
from webui import Handler
# 文件挂载
Base_api.mount(
path="/static",
app=StaticFiles(directory=STATIC),
name="static",
)
class ProcessExecutor:
def __init__(self):
self.tasks = []
def add_task(self, func, *args, **kwargs):
self.tasks.append((func, args, kwargs))
def start(self):
processes = []
with multiprocessing.Pool() as pool:
for task in self.tasks:
func, args, kwargs = task
process = pool.apply_async(func, args=args, kwds=kwargs)
processes.append(process)
for process in processes:
process.get()
class WebModel(BaseModel):
"""
web 模型
"""
reload: bool = True
host: Optional[str] = "localhost"
port: Optional[int] = 8090
def web_server(conf: WebModel = WebModel()) -> bool:
srv: Server = Server(Config(
app=Base_api,
host=conf.host,
port=conf.port,
reload=conf.reload,
))
srv.run()
return srv.started
def web_ui():
import webview.menu as wm
app = Handler()
app.add_menu(
'服务选择',
[
wm.MenuAction('前端', app.frontend),
wm.MenuSeparator(),
wm.MenuAction("后端", app.backend),
],
)
app.add_menu(
'主页',
[
wm.MenuAction('运行状况', app.index),
wm.MenuSeparator(),
wm.MenuAction('自适应窗口', app.auto_resize),
],
)
app.start_webview()
if __name__ == "__main__":
executor = ProcessExecutor()
executor.add_task(web_ui)
executor.add_task(web_server)
try:
executor.start()
except (Exception, RuntimeError) as _:
pass
# import asyncio
#
# from utils.common import BaseClient
# from utils.config import Config, log
# from utils.model.agent import AgentModel, ChatModel, TaskModel
#
#
# class AsyncQueue:
# """
# 异步队列
# """
#
# def __init__(self):
# self.queue = asyncio.Queue()
#
# async def put(self, item):
# await self.queue.put(item)
#
# async def get(self):
# return await self.queue.get()
#
# def empty(self):
# return self.queue.empty()
#
# def size(self):
# return self.queue.qsize()
#
#
# async def process_queue(queue):
# while not queue.empty():
# item: TaskModel = await queue.get()
# # 处理item
# try:
# reply = await item.execute()
# except (Exception, RuntimeError) as err:
# log.exception(err)
# else:
# log.info(reply)
#
#
# print(Config.get_select_config())
# client = BaseClient(
# ChatModel(**Config.get_select_config())
# )
# researcher = AgentModel(
# role='Researcher',
# backstory="You're a world class researcher working on a major data science company",
# client=client,
# )
# writer = AgentModel(
# role='Writer',
# backstory="You're a famous technical writer, specialized on writing data related content",
# client=client,
# )
#
# task1 = TaskModel(
# agent=researcher,
# description="你好"
# )
#
# task2 = TaskModel(
# agent=writer,
# description="你是谁?"
# )
#
#
# async def task():
# # 加入数据到队列
# async_queue = AsyncQueue()
# # 添加队列任务
# await async_queue.put(
# task1,
# )
# await async_queue.put(
# task2,
# )
# # 异步处理队列
# await process_queue(async_queue)
#
#
# asyncio.run(task())
# if __name__ == '__main__':
#
# asyncio.run(
# task()
# )
# if __name__ == '__main__':
# import asyncio
#
# client = BaseClient(
# ChatModel(**Config.get_select_config())
# )
# # log.info(client.send_to_chatgpt("你好"))
# asyncio.run(client.send_to_chatgpt(
# [
# {
# "role": "user",
# "content": "你好!"
# }
# ]
# ))

6
requirement.txt Normal file
View File

@ -0,0 +1,6 @@
fastapi[all]
pymysql
docx2txt
tiktoken
pywebview
dbutils

14
res/prompt.key Normal file
View File

@ -0,0 +1,14 @@
将软件设计文档中的功能、角色、实体以及它们之间的关系进行提取, 并转化为类似于以下示例的JSON格式。总内容不能超过400字。 `//`中包含一些特殊要求
```json
{
"function": {
"学生管理": {"relation": "可以创建和管理"},
"房间分配": {"relation": "房间分配与管理"},
"功能点": {"relation": "实体关系"},
// 添加更多的功能点机器关系|操作对象,字段的值可以为""
},
"summarize": "简要总结一下设计文档中实现的内容,用作后续的上下文提示 prompt控制在 200 字以内.",
"new_summarize": "指出设计文档中还能继续增加的功能,并指出添加该功能的原因。分点输出 控制在 200 字内。"
}
```
被```rule $设计文档 ``` 包裹的内容为需要转化的内容, 只需输出完整的json转换内容即可.

View File

@ -2,9 +2,135 @@
"""
@Desc : 代理执行器
"""
import json
import random
import re
from datetime import datetime
from typing import Optional
from utils.common import BaseClient
from utils.config import Config, log
from utils.model.agent import AgentModel
class Executor:
"""
执行器
"""
class SummarizeRobot:
def __init__(self):
self.prompt = self.__load_prompt
def robot(self, client: BaseClient) -> AgentModel:
return AgentModel(
role='总结机器人',
backstory=self.prompt,
client=client,
)
@property
def __load_prompt(self) -> str:
file = str(Config.get_path("res", "prompt.key"))
with open(file, "r", encoding="utf-8") as file_obj:
return file_obj.read()
@staticmethod
def generate_frontend_data(keys: list) -> list:
result = [
{
"name": key,
"itemStyle": {
"color": f'rgb({random.randint(0, 255)},{random.randint(0, 255)},{random.randint(0, 255)})'
}
} for key in keys
]
return result
def complete_data(self, resp: str):
""" 数据补全 """
result = {"clean": [], 'origin': self.clean_parse_data(resp)}
if result['origin'] is None:
raise RuntimeError(f"数据为空")
try:
# 提取function中所有的第一层键
func_keys = list(result['origin']['function'].keys())
# 基础功能点
result['clean'].append(self.generate_frontend_data(func_keys))
except Exception as err:
log.debug(err)
raise RuntimeError(err)
return result
def clean_parse_data(self, input_str: str):
""" 数据清洗 """
parsed_content: Optional[dict] = None
if len(result := re.findall('```json(.*?)```', input_str)):
reply = json.loads(result[0])
log.info(reply)
self.save_to_file(reply)
try:
try:
parsed_content = json.loads(input_str)
except json.JSONDecodeError as _:
input_str = input_str.replace("`", "").strip("\n")
try:
# 寻找第一个 "{" 的位置
first_brace_index = input_str.find("{")
# 寻找最后一个 "}" 的位置
last_brace_index = input_str.rfind("}")
input_str = input_str[first_brace_index:last_brace_index + 1]
except IndexError or Exception as err:
log.warn(err)
pass
# 使用正则表达式提取大括号内的内容
matches = re.findall(r'\{(.*)}', input_str)
if matches:
# 获取匹配到的内容
extracted = matches[0] # 假设只有一个匹配,你可以根据实际情况调整
# 尝试将提取的内容反序列化为JSON
parsed_content = json.loads("{" + extracted + "}")
else:
# 如果没有匹配到大括号,抛出异常
raise Exception("未找到大括号内的内容")
finally:
log.warn(parsed_content)
if isinstance(parsed_content, dict):
self.save_to_file(parsed_content)
# 返回反序列化后的JSON
return parsed_content
except Exception as err:
log.exception(f"发生异常: {err}")
raise RuntimeError(err)
@staticmethod
def save_to_file(parsed_content):
try:
# 获取当前时间作为文件名
date_line = datetime.now().strftime('%Y%m%d%H%M%S')
filename = Config.get_path("res", "gen", f"{date_line}.json")
with open(filename, "w", encoding="utf-8") as file_obj:
file_obj.write(json.dumps(parsed_content, ensure_ascii=False))
log.info(f"文件 '{date_line}.json' 已保存至 res/gen 目录中")
except Exception as e:
# 捕获写入文件时的异常并抛出
raise Exception(f"写入文件时发生异常: {str(e)}")

View File

@ -0,0 +1,9 @@
# -*- encoding: utf-8 -*-
"""
用户鉴权
>>> from utils.auth.auth import AuthParams
>>> username = "demo"
>>> AuthParams.Delete(username)
"""
from .auth import AuthParams

View File

@ -0,0 +1,116 @@
# -*- encoding: utf-8 -*-
"""
@GitHub: https://github.com/Fromsko
@Author: Fromsko
@Desc : 鉴权
"""
import pymysql
from utils.auth.model import JsonMsg
from utils.database.storage import MySQLStorage
class AuthParams(MySQLStorage, JsonMsg):
def __init__(self, conf: dict):
super().__init__(conf)
self.create_user_table()
def query(self, user: str):
"""查询数据
参数:
`user`: 用户
返回:
- `msg`: 统一格式数据
"""
result = self.execute_query(
"SELECT * FROM users WHERE username = %s",
(user,)
)
if not result:
self.base_msg['msg'] = self.Query.notfound
else:
self.base_msg['msg'] = result[0]
return self.base_msg
def insert(self, user, hash_passwd):
"""插入数据
参数:
- `user`: 用户
- `hash_password`: 哈希后的密码
返回:
- `msg`: 统一格式数据
"""
try:
self.execute_update(
"INSERT INTO users (username, password) VALUES (%s, %s)",
(user, hash_passwd)
)
self.base_msg['msg'] = self.Insert.succeed
except pymysql.IntegrityError as _:
self.base_msg['msg'] = self.Query.exits
return self.base_msg
def update(self, user, new_password):
"""更新数据
参数:
- `user`: 用户
- `new_password`: 新密码
返回:
- `msg`: 统一格式数据
"""
query_result = self.query(user)
if query_result['msg'] == self.Query.notfound:
return query_result
try:
self.execute_update(
"UPDATE users SET password = %s WHERE username = %s",
(new_password, user)
)
self.base_msg['msg'] = self.Update.succeed
except Exception as err:
self.base_msg['msg'] = self.Update.failed.format(err)
return self.base_msg
def delete(self, user):
"""删除数据
参数:
`user`: 待删除用户
返回:
- `msg`: 统一格式数据
"""
query_result = self.query(user)
if query_result['msg'] == self.Query.notfound:
return query_result
try:
self.execute_update(
"DELETE FROM users WHERE username = %s",
(user,)
)
self.base_msg['msg'] = self.Delete.succeed
except Exception as err:
self.base_msg['msg'] = self.Delete.failed.format(err)
return self.base_msg
__all__ = [
"AuthParams",
]

View File

@ -0,0 +1,27 @@
# -*- encoding: utf-8 -*-
"""
@GitHub: https://github.com/Fromsko
@Author: Fromsko
@Desc : 数据模型
"""
import time
class JsonMsg:
base_msg = {"code": 200, "msg": "", "dateline": time.time()}
class Query:
exits = "用户已存在"
notfound = "用户不存在"
class Insert:
succeed = "添加成功"
failed = "添加失败"
class Update:
succeed = "更新成功"
failed = "更新失败 {err}"
class Delete:
succeed = "删除成功"
failed = "删除失败 {err}"

View File

@ -0,0 +1,9 @@
# -*- encoding: utf-8 -*-
"""
GPT模型
"""
from .chatgpt import BaseClient
__all__ = [
"BaseClient"
]

View File

@ -0,0 +1,62 @@
# -*- encoding: utf-8 -*-
"""
@Desc : GPTModel
"""
import json
from typing import Any, Optional, Union
import httpx
from utils.config import log
from utils.model.agent import ChatAbstractModel, ChatModel
from utils.model.error import GPTAPIError
class BaseClient(ChatAbstractModel):
"""
基础模型
"""
def __init__(self, chat_model: ChatModel):
self.api_key = chat_model.api_key
self.api_base = chat_model.api_base
self.proxy = chat_model.proxy if chat_model.status else None
async def _fetch(self, payload) -> Optional[dict]:
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': f'Bearer {self.api_key}',
}
async with httpx.AsyncClient(proxies=self.proxy, timeout=120) as client:
resp = await client.post(
f'{self.api_base}/chat/completions',
headers=headers,
data=payload,
)
return resp.json()
async def send_to_chatgpt(self, prompt: list[dict]) -> Union[dict, str]:
"""发送信息"""
payload = json.dumps({
"model": "gpt-3.5-turbo-16k",
"messages": prompt,
"temperature": 0.2,
"presence_penalty": 0,
"max_tokens": 3000,
})
try:
resp = await self._fetch(payload)
except (TimeoutError, ConnectionError) as err:
log.exception(err)
return GPTAPIError.timeout_error
try:
reply = resp['choices'][0]['message']['content']
except (IndexError, RuntimeError, TypeError, KeyError) as err:
log.error(err)
return GPTAPIError.server_error
else:
log.info(f"实际消耗: {resp['usage']['total_tokens']}")
return reply

View File

@ -0,0 +1,55 @@
# -*- encoding: utf-8 -*-
"""
@Desc : 模型自定义
"""
import asyncio
class AsyncQueue:
"""
异步队列
"""
def __init__(self):
self.queue = asyncio.Queue()
async def put(self, item):
await self.queue.put(item)
async def get(self):
return await self.queue.get()
def empty(self):
return self.queue.empty()
def size(self):
return self.queue.qsize()
"""
async def process_queue(queue):
while not queue.empty():
item = await queue.get()
# 处理item
print(item)
async def task():
# 加入数据到队列
await async_queue.put('item1')
await async_queue.put('item2')
await async_queue.put('item3')
# 异步处理队列
await process_queue(async_queue)
if __name__ == '__main__':
async_queue = AsyncQueue()
asyncio.run(
task()
)
"""

View File

@ -0,0 +1,19 @@
# -*- encoding: utf-8 -*-
"""
配置
>>> from utils.config import Config
>>> Config.config
>>> Config.get_path("res")
>>> Config.get_select_config("chat")
"""
from .base import BaseConfig, hash_password, log
from .easy import EasyTool
Config = BaseConfig()
__all__ = [
"hash_password",
"Config",
"log",
]

View File

@ -0,0 +1,81 @@
# -*- encoding: utf-8 -*-
"""
@Desc : 基础配置
"""
import hashlib
import yaml
from utils.config.easy import EasyTool
from utils.log import Logger
log = Logger("KnowledgeMap")
class BaseConfig(EasyTool):
def __init__(self):
super().__init__()
self.config = self.load_config()
def load_config(self):
"""导入配置文件
Returns:
_type_: dict
"""
if self.config_file.exists():
with open(f"{self.config_file}", "r", encoding="utf-8") as file:
config = yaml.safe_load(file)
else:
self.save_config(self.yaml_config)
log.exception("未找到配置文件, 正在生成!")
exit()
return config
def save_config(self, config):
with open(f"{self.config_file}", "w", encoding="utf-8") as file:
yaml.dump(config, file, default_flow_style=False)
@property
def yaml_config(self):
config = {
"mysql": {
"host": "127.0.0.1",
"user": "root",
"password": "123456",
"database": "knowlege",
},
"chat": {
"status": False,
"api_key": "default_api_key",
"proxy": "http://localhost:7890",
"api_base": "https://api.aigcbest.top/v1",
}
}
return config
def get_select_config(self, select_key: str = "chat") -> dict:
"""获取选择的配置
参数:
- `select_key`: 选择的键
返回:
- `dict`: 字典
"""
conf = self.config[select_key]
if select_key != "chat": # 转换密码
conf['password'] = f"{conf['password']}"
return conf
def hash_password(password):
""" hash for password """
return hashlib.sha256(password.encode()).hexdigest()
__all__ = [
"hash_password",
"BaseConfig",
"log"
]

View File

@ -0,0 +1,35 @@
# -*- encoding: utf-8 -*-
"""
@Desc : 基类
"""
from pathlib import Path
class EasyTool(object):
def __init__(self) -> None:
self._gen_dir(["res", "frontend", "res/prompts"])
self.config_file = self.get_path("config.yaml")
@property
def work_dir(self):
""" Return to work path """
return Path.cwd()
def get_path(self, *args) -> Path:
""" get dir/file path """
local = self.work_dir.joinpath(*args)
if len(local.name.split('.')) == 0:
if not local.exists():
local.mkdir()
else:
if not local.parent.exists():
local.parent.mkdir()
return local
def _gen_dir(self, dir_list=None):
""" generate dirs """
for dir_ in dir_list:
if (dir_ := self.work_dir.joinpath(dir_)).exists():
continue
dir_.mkdir()

View File

@ -0,0 +1,14 @@
# -*- encoding: utf-8 -*-
"""
数据库存储
>>> from utils.config import Config
>>> from utils.database import MySQLStorage
>>> db = MySQLStorage(Config.get_select_config("mysql"))
>>> db.execute_query("SELECT * FROM Users;")
"""
from .storage import MySQLStorage
__all__ = [
"MySQLStorage"
]

View File

@ -0,0 +1,68 @@
# -*- encoding: utf-8 -*-
"""
@Desc : 数据存储
"""
import pymysql
from dbutils.pooled_db import PooledDB
from utils.config import log
class MySQLStorage:
_pool = None
def __init__(self, conf: dict):
self.conn = self.load_config(conf)
self.cursor = self.conn.cursor()
@classmethod
def get_connection_pool(cls, conf: dict):
if not cls._pool:
cls._pool = PooledDB(
creator=pymysql,
mincached=5,
maxcached=20,
**conf
)
return cls._pool
@staticmethod
def load_config(conf):
try:
pool = MySQLStorage.get_connection_pool(conf)
conn = pool.connection()
except Exception as err:
raise RuntimeError(f"连接 MYSQL 错误: {err}")
return conn
def execute_query(self, query, params=None):
self.cursor.execute(query, params)
return self.cursor.fetchall()
def execute_update(self, query, params=None):
self.cursor.execute(query, params)
self.conn.commit()
def create_user_table(self):
query = """
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
self.execute_update(query)
def __del__(self):
try:
self.cursor.close()
self.conn.close()
except RuntimeError as err:
log.excption(err)
exit()
__all__ = [
"MySQLStorage",
]

View File

@ -0,0 +1,50 @@
# -*- encoding: utf-8 -*-
"""
@Desc : 日志类
"""
import wrapt
from loguru import logger
class Logger:
def __init__(self, name):
self.__log = logger.bind(name=name)
self.__log.add(
sink=f"{name}.log",
format="{time} | {level} | {message}",
rotation="1 week",
)
def __call__(self, func):
@wrapt.decorator
def wrapper(wrapped, instance, args, kwargs):
resp = wrapped(*args, **kwargs)
self.info(f"{wrapped.__name__}|=> {resp}")
return resp
return wrapper(func)
@property
def info(self):
return self.__log.info
@property
def warn(self):
return self.__log.warning
@property
def error(self):
return self.__log.error
@property
def debug(self):
return self.__log.debug
@property
def exception(self):
return self.__log.exception
__all__ = [
'Logger',
]

View File

@ -0,0 +1,41 @@
# -*- encoding: utf-8 -*-
"""
数据模型
```markdown
## 分类
1. 观察者模式
2. 适配器模式
3. 错误定义
4. 数据模型
```
---
使用
```python
>>> from utils.model.error import *
>>> chat_model = ChatModel(**{"api_base": "", "api_key": ""})
>>> chat_model.api_base
>>> chat_model.api_key
```
"""
from .adapter import DocumentAdapter, Docx, Doc, Txt
from .agent import ChatModel
from .error import GPTAPIError, UploadAPIError, UploadFileError
from .observer import Notice, Observer
__all__ = [
# GPT模型
"ChatModel",
# 文档
"DocumentAdapter",
"Doc",
"Docx",
"Txt",
# 观察者模式
"Observer",
"Notice",
# 错误
"UploadAPIError",
"UploadFileError",
"GPTAPIError",
]

View File

@ -0,0 +1,136 @@
# -*- encoding: utf-8 -*-
"""
@Desc : 适配器模式
> 转化不同格式的文档 {docx | doc | txt} => txt
"""
from abc import ABCMeta, abstractmethod
from pathlib import Path
from typing import BinaryIO, Union
from docx import Document
from docx2txt import docx2txt
from fastapi import UploadFile as File
from utils.config import Config, log
from utils.model.error import UploadFileError
class UploadFile(metaclass=ABCMeta):
@abstractmethod
def parser(self, file: Union[str, Path, bytes, File]) -> str:
"""解析器
参数:
- `file`: 文件 | 文件路径
返回:
- `str`: 文件内容
"""
pass
def save_upload_file(file: File) -> str:
file.filename = str(Config.get_path(
"res", "upload",
file.filename,
))
try:
with open(file.filename, "wb") as f:
f.write(file.file.read())
except (IOError, Exception):
log.warn(UploadFileError.save_error)
else:
return file.filename
def convert_doc_to_docx(doc_file, docx_file) -> str:
doc = Document(doc_file)
doc.save(docx_file)
return docx_file
def get_filename_info(file_path):
path = Path(file_path)
file_name = path.name
file_name_without_extension = path.stem
parent_directory = path.parent
return file_name, file_name_without_extension, parent_directory
class Docx(UploadFile):
"""
解析 Docx 类型文档
"""
def parser(self, file: Union[str, Path, bytes, File]) -> str:
save_path: str = save_upload_file(file)
try:
txt = docx2txt.process(save_path)
except (IOError, Exception):
log.warn(UploadFileError.parser_error)
else:
return txt
return ""
class Doc(UploadFile):
"""
解析 Doc 类型文档
"""
def parser(self, file: Union[str, Path, bytes, File]) -> str:
save_path: str = save_upload_file(file)
_, filename, file_dir = get_filename_info(save_path)
try:
save_path = convert_doc_to_docx(
save_path,
file_dir.joinpath(filename + ".docx"),
)
txt = docx2txt.process(save_path)
except (IOError, Exception):
log.warn(UploadFileError.parser_error)
else:
return txt
return ""
class Txt(UploadFile):
"""
解析 txt 类型文档
"""
def parser(self, file: Union[str, Path, bytes, File]) -> str:
file: BinaryIO = file.file
try:
# 使用文件指针到首位
file.seek(0)
except (IOError, Exception):
log.warn(UploadFileError.move_error)
else:
content: bytes = file.read()
return content.decode('utf-8')
return ""
class DocumentAdapter(UploadFile):
def __init__(self, mode: UploadFile):
self.mode: UploadFile = mode
def parser(self, file: Union[str, Path, bytes, File]) -> str:
return self.mode.parser(file)
__all__ = [
"DocumentAdapter",
"Docx",
"Doc",
"Txt",
]

View File

@ -4,9 +4,11 @@
> 用于校验数据格式
"""
from typing import Optional
import uuid
from typing import Any, Optional, Union
from pydantic import BaseModel, Field, UUID4
from pydantic import BaseModel
from utils.config import log
class ChatModel(BaseModel):
@ -17,4 +19,97 @@ class ChatModel(BaseModel):
# 请求代理
proxy: Optional[str]
# 是否代理
flag: bool = False
status: bool = False
class ChatAbstractModel(object):
async def send_to_chatgpt(self, prompt: list[dict]) -> Union[dict, str]:
"""发送至GPT模型
参数:
- `prompt`: 请求载体
返回:
- `Union[dict, str]`: 错误 | 信息
"""
class MsgModel(BaseModel):
id: UUID4 = Field(
default_factory=uuid.uuid4,
frozen=True,
description="Unique identifier for the object, not set by user.",
)
prompt: Optional[list[dict]] = Field(
default=[{
"role": "system",
"content": "You are a large model robot that is very good at summarizing,"
" which is suitable for software engineering and knowledge engineering"
}],
description="Prompt for the agent!",
)
content: Optional[str] = None
class AgentModel(BaseModel):
# 连接
client: Optional[Any] = None
# 角色
role: str = Field(
description="Role of the agent",
)
# 背景
backstory: Optional[str] = Field(
description="Backstory of the agent",
)
async def execute_task(
self, task: str,
context: Optional[MsgModel] = None,
) -> Union[dict, str]:
self.client: ChatAbstractModel
if self.role is not None:
log.info(f"Agent {self.role} is running...")
if self.backstory is not None:
context.prompt.append({"role": "system", "content": self.backstory})
context.prompt.append({"role": "user", "content": task})
if self.client is None:
raise RuntimeError("Agent {role} client is not start!")
return await self.client.send_to_chatgpt(
context.prompt,
)
class TaskModel(BaseModel):
__hash__ = object.__hash__
description: str = Field(
description="Description of the agent task.",
)
agent: Optional[AgentModel] = Field(
description="Agent factory", default=None
)
result_collection: Optional[dict] = {}
async def execute(self, context: Optional[MsgModel] = MsgModel()) -> Union[dict, tuple]:
if self.agent is not None:
reply = await self.agent.execute_task(
task=self.description,
context=context,
)
if isinstance(reply, dict): # 错误信息
return reply
self.result_collection[self.agent.role] = {
"id": context.id,
"result": reply,
"content": context.content,
}
return self.result_collection
raise Exception(f"{self.description} is not a task.")

View File

@ -0,0 +1,41 @@
# -*- encoding: utf-8 -*-
"""
@Desc : 错误模型定义
"""
from typing import Optional, Union
class UploadFileError:
move_error: Optional[Union[IOError, str]] = "文件指针移动失败"
parser_error: Optional[Union[IOError, str]] = "解析文档异常"
save_error: Optional[Union[IOError, str]] = "文件存储错误"
class UploadAPIError:
empty_upload_error: dict = {
"code": 400,
"err": "",
}
type_upload_error: dict = {
"code": 400,
"err": "文件格式错误",
}
parser_upload_error: dict = {
"code": 400,
"err": "文档解析失败",
}
size_upload_error: dict = {
"code": 400,
"err": "文件超过大小",
}
class GPTAPIError:
timeout_error: dict = {
"code": 400,
"err": "请求超时",
}
server_error: dict = {
"code": 500,
"err": "服务器异常",
}

View File

@ -0,0 +1,69 @@
# -*- encoding: utf-8 -*-
"""
@Desc : 观察者模式
> 用来通知事件是否成功运行 | 处理对应的事情
"""
from abc import ABCMeta, abstractmethod
class Observer(metaclass=ABCMeta): # 观察者
@abstractmethod
def update(self, notice): # Notice类对象
pass
class Notice: # 发布者
def __init__(self):
self.observers: list[Observer] = []
def attach(self, obs: Observer):
self.observers.append(obs)
def detach(self, obs: Observer):
self.observers.remove(obs)
def notify(self):
for obs in self.observers:
obs.update(self)
class StaffNotice(Notice):
def __init__(self, company_info=None):
super().__init__()
self.__company_info = company_info
@property
def company_info(self):
return self.__company_info
@company_info.setter
def company_info(self, info):
self.__company_info = info
self.notify()
class Staff(Observer):
def __init__(self):
self.company_info = None
def update(self, notice: StaffNotice):
self.company_info = notice.company_info
if __name__ == '__main__':
notify = StaffNotice("小米")
s1 = Staff()
s2 = Staff()
notify.attach(s1)
notify.attach(s2)
notify.company_info = "Hello World!"
print(s1.company_info, s2.company_info)
notify.company_info = "Hello!"
print(s1.company_info, s2.company_info)

236
webui.py Normal file
View File

@ -0,0 +1,236 @@
"""
@Author: skong
@File : webui.py
@notes : Web UI with backend
"""
from typing import Any, List, Optional
import webview
import webview.menu as wm
from pydantic import BaseModel
from webview import Window
from utils.log import Logger
class Config:
"""
配置基类
"""
_version = 3.6
_base_name: str = "知识图谱"
_base_url: str = "http://localhost:8090"
_web: webview = webview
_menu_items: List = []
_default_view: Optional[Window] = None
log: Logger = Logger("webui")
__html = """
<head>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
background-color: #f2f2f2;
font-family: Arial, sans-serif;
}
.container {
text-align: center;
width: auto;
height: auto;
}
h1 {
font-size: 24px;
margin-bottom: 20px;
}
button {
padding: 10px 20px;
font-size: 18px;
background-color: #4CAF50;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="container">
<h1>服务已经运行!!!</h1>
<button onclick="pywebview.api.close_window()">关闭程序</button>
</div>
</body>
"""
Router: dict[Any] = {
"frontend": {
"title": f"{_base_name}-前端",
"url": f"{_base_url}/home",
"html": "",
},
"backend": {
"title": f"{_base_name}-后端",
"url": f"{_base_url}/docs",
"html": "",
},
"index": {
"title": f"{_base_name}-{_version}",
"url": "",
"html": __html,
},
}
def get_config(self, key: str, view: Optional[Window]) -> Any:
"""获取配置信息
参数:
- `key`: 需要获取的键
- `view`: 视图(不存在则获取本地配置)
示例:
```python
# 默认视图
self.get_config('window.screen.width', default_view)
# 配置信息
self.get_config('version')
```
"""
if view is not None:
return view.evaluate_js(key)
else:
...
def auto_resize(
self,
width: Optional[int] = None,
height: Optional[int] = None,
view: Optional[Window] = None,
):
"""自适应窗口大小
参数:
- `view`: 视图
- `width`: 宽度
- `height`: 高度
示例:
```python
self.auto_resize(view)
```
"""
if view is None:
view = self._default_view
if isinstance(view, Window):
if width is None and height is None:
screens = self._web.screens[0]
width = int(screens.width * 0.85)
height = int(screens.height * 0.85)
view.resize(width, height)
else:
self.log.error("Window is not found")
def close_window(self, window: Window):
"""
关闭窗口
"""
self._default_view.destroy()
class MenuModel(BaseModel):
"""
菜单数据模型
"""
title: Optional[str]
url: Optional[str]
html: Optional[str]
class Handler(Config):
def add_menu(self, key: str, view: list[Any]):
"""
增加菜单
"""
try:
self._menu_items.insert(0, wm.Menu(key, view))
except Exception as err:
self.log.error(err)
def frontend(self):
"""
前端
"""
return self.render(MenuModel(**self.Router['frontend']))
def backend(self):
"""
后端
"""
return self.render(MenuModel(**self.Router['backend']))
def index(self):
"""
主页
"""
return self.render(MenuModel(**self.Router['index']))
def render(self, info: MenuModel):
"""渲染页面
参数:
- `info`: 数据模型或者空类型
示例:
```python
def index(self):
return self.render(info=self.Router['index'])
```
"""
self.auto_resize()
self._default_view.title = info.title
if info.html != "":
self._default_view.load_html(info.html)
else:
self._default_view.load_url(info.url)
def start_webview(self):
"""
启动 `WebView` 程序
"""
self._default_view = self._web.create_window(
self.Router['index']['title'],
html=self.Router['index']['html'],
# confirm_close=True,
)
self._default_view.expose(self.close_window)
self._web.start(menu=self._menu_items)
if __name__ == '__main__':
app = Handler()
app.add_menu(
'服务选择',
[
wm.MenuAction('前端', app.frontend),
wm.MenuSeparator(),
wm.MenuAction("后端", app.backend),
],
)
app.add_menu(
'主页',
[
wm.MenuAction('运行状况', app.index),
wm.MenuSeparator(),
wm.MenuAction('自适应窗口', app.auto_resize),
],
)
app.start_webview()