fix: 1. 增加 ollama vl 模型配置 2. 优化简易后台
This commit is contained in:
parent
ccf8bd6902
commit
eaa6fc6fe7
|
|
@ -11,6 +11,7 @@ LLM_MODEL=qwen2.5-7b-awq
|
||||||
LLM_KEY=EMPTY # vLLM default key
|
LLM_KEY=EMPTY # vLLM default key
|
||||||
|
|
||||||
# LLM(Vision) Configuration
|
# LLM(Vision) Configuration
|
||||||
|
VL_BINDING=vllm # ollama, vllm, openai
|
||||||
VL_BINDING_HOST=http://192.168.6.115:8001/v1
|
VL_BINDING_HOST=http://192.168.6.115:8001/v1
|
||||||
VL_MODEL=qwen2.5-vl-3b-awq
|
VL_MODEL=qwen2.5-vl-3b-awq
|
||||||
VL_KEY=EMPTY
|
VL_KEY=EMPTY
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ class Settings(BaseSettings):
|
||||||
LLM_KEY: str = "EMPTY" # vLLM default key
|
LLM_KEY: str = "EMPTY" # vLLM default key
|
||||||
|
|
||||||
# LLM (Vision) - vLLM
|
# LLM (Vision) - vLLM
|
||||||
|
VL_BINDING: str = "vllm" # ollama, vllm, openai
|
||||||
VL_BINDING_HOST: str = "http://192.168.6.115:8001/v1"
|
VL_BINDING_HOST: str = "http://192.168.6.115:8001/v1"
|
||||||
VL_MODEL: str = "qwen2.5-vl-3b-awq"
|
VL_MODEL: str = "qwen2.5-vl-3b-awq"
|
||||||
VL_KEY: str = "EMPTY"
|
VL_KEY: str = "EMPTY"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ from app.config import settings
|
||||||
|
|
||||||
async def vl_image_caption_func(image_data: bytes, prompt: str = "请详细描述这张图片") -> str:
|
async def vl_image_caption_func(image_data: bytes, prompt: str = "请详细描述这张图片") -> str:
|
||||||
"""
|
"""
|
||||||
使用 VL 模型 (vLLM OpenAI API) 生成图片描述
|
使用 VL 模型生成图片描述
|
||||||
|
支持 ollama 和 openai/vllm 协议
|
||||||
"""
|
"""
|
||||||
if not settings.VL_BINDING_HOST:
|
if not settings.VL_BINDING_HOST:
|
||||||
return "[Image Processing Skipped: No VL Model Configured]"
|
return "[Image Processing Skipped: No VL Model Configured]"
|
||||||
|
|
@ -15,38 +16,53 @@ async def vl_image_caption_func(image_data: bytes, prompt: str = "请详细描
|
||||||
# 1. 编码图片为 Base64
|
# 1. 编码图片为 Base64
|
||||||
base64_image = base64.b64encode(image_data).decode('utf-8')
|
base64_image = base64.b64encode(image_data).decode('utf-8')
|
||||||
|
|
||||||
# 2. 构造 OpenAI 格式请求
|
|
||||||
# vLLM 支持 OpenAI Vision API
|
|
||||||
url = f"{settings.VL_BINDING_HOST}/chat/completions"
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": f"Bearer {settings.VL_KEY}"
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"model": settings.VL_MODEL,
|
|
||||||
"messages": [
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": [
|
|
||||||
{"type": "text", "text": prompt},
|
|
||||||
{
|
|
||||||
"type": "image_url",
|
|
||||||
"image_url": {
|
|
||||||
"url": f"data:image/jpeg;base64,{base64_image}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"max_tokens": 300
|
|
||||||
}
|
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
response = await client.post(url, headers=headers, json=payload)
|
if settings.VL_BINDING == "ollama":
|
||||||
response.raise_for_status()
|
# Ollama 协议
|
||||||
result = response.json()
|
url = f"{settings.VL_BINDING_HOST}/api/generate"
|
||||||
description = result['choices'][0]['message']['content']
|
payload = {
|
||||||
|
"model": settings.VL_MODEL,
|
||||||
|
"prompt": prompt,
|
||||||
|
"images": [base64_image],
|
||||||
|
"stream": False
|
||||||
|
}
|
||||||
|
response = await client.post(url, json=payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
result = response.json()
|
||||||
|
description = result.get('response', '')
|
||||||
|
|
||||||
|
else:
|
||||||
|
# OpenAI / vLLM 协议
|
||||||
|
url = f"{settings.VL_BINDING_HOST}/chat/completions"
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {settings.VL_KEY}"
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": settings.VL_MODEL,
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": prompt},
|
||||||
|
{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {
|
||||||
|
"url": f"data:image/jpeg;base64,{base64_image}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max_tokens": 300
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await client.post(url, headers=headers, json=payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
result = response.json()
|
||||||
|
description = result['choices'][0]['message']['content']
|
||||||
|
|
||||||
return f"[Image Description: {description}]"
|
return f"[Image Description: {description}]"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -70,14 +86,14 @@ async def process_pdf_with_images(file_bytes: bytes) -> str:
|
||||||
text_content += f"--- Page {page_num + 1} Text ---\n{page_text}\n\n"
|
text_content += f"--- Page {page_num + 1} Text ---\n{page_text}\n\n"
|
||||||
|
|
||||||
# 2. 提取图片
|
# 2. 提取图片
|
||||||
if False and settings.VL_BINDING_HOST:
|
if settings.VL_BINDING_HOST:
|
||||||
for count, image_file_object in enumerate(page.images):
|
for count, image_file_object in enumerate(page.images):
|
||||||
try:
|
try:
|
||||||
# 获取图片数据
|
# 获取图片数据
|
||||||
image_data = image_file_object.data
|
image_data = image_file_object.data
|
||||||
|
|
||||||
# 简单验证图片有效性
|
# 简单验证图片有效性
|
||||||
# Image.open(BytesIO(image_data)).verify()
|
Image.open(BytesIO(image_data)).verify()
|
||||||
|
|
||||||
# 调用 VL 模型
|
# 调用 VL 模型
|
||||||
caption = await vl_image_caption_func(image_data)
|
caption = await vl_image_caption_func(image_data)
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
<h1 @click="goHome" style="cursor: pointer;">LightRAG Admin</h1>
|
<h1 @click="goHome" style="cursor: pointer;">LightRAG Admin</h1>
|
||||||
<div v-if="currentPage === 'tenant'">
|
<div v-if="currentPage === 'tenant'">
|
||||||
<el-tag type="success" size="large">当前租户: {{ currentTenantId }}</el-tag>
|
<el-tag type="success" size="large">当前租户: {{ currentTenantId }}</el-tag>
|
||||||
<el-button type="primary" link @click="goHome">返回首页</el-button>
|
<el-button v-if="isAdmin" type="primary" link @click="goHome">返回首页</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -113,41 +113,44 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 导入弹窗 -->
|
<!-- 导入弹窗 -->
|
||||||
<el-dialog v-model="showImportDialog" title="导入知识" width="600px">
|
<el-dialog v-model="showImportDialog" title="导入知识" width="600px" :close-on-click-modal="!importing" :close-on-press-escape="!importing" :show-close="!importing">
|
||||||
<el-tabs v-model="importType">
|
<div v-loading="importing" element-loading-text="正在导入中,请稍候...(大文件可能需要较长时间)">
|
||||||
<el-tab-pane label="文件上传" name="file">
|
<el-tabs v-model="importType">
|
||||||
<el-upload
|
<el-tab-pane label="文件上传" name="file">
|
||||||
class="upload-demo"
|
<el-upload
|
||||||
drag
|
class="upload-demo"
|
||||||
action="#"
|
drag
|
||||||
:http-request="uploadFile"
|
action="#"
|
||||||
:show-file-list="false"
|
:http-request="uploadFile"
|
||||||
>
|
:show-file-list="false"
|
||||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
:disabled="importing"
|
||||||
<div class="el-upload__text">拖拽文件到此处或 <em>点击上传</em></div>
|
>
|
||||||
<template #tip>
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
<div class="el-upload__tip">支持 .txt, .md, .pdf</div>
|
<div class="el-upload__text">拖拽文件到此处或 <em>点击上传</em></div>
|
||||||
</template>
|
<template #tip>
|
||||||
</el-upload>
|
<div class="el-upload__tip">支持 .txt, .md, .pdf</div>
|
||||||
</el-tab-pane>
|
</template>
|
||||||
<el-tab-pane label="纯文本" name="text">
|
</el-upload>
|
||||||
<el-input v-model="importText" type="textarea" rows="10" placeholder="请输入文本内容..."></el-input>
|
</el-tab-pane>
|
||||||
<div style="margin-top: 10px; text-align: right;">
|
<el-tab-pane label="纯文本" name="text">
|
||||||
<el-button type="primary" @click="uploadText">提交</el-button>
|
<el-input v-model="importText" type="textarea" rows="10" placeholder="请输入文本内容..." :disabled="importing"></el-input>
|
||||||
</div>
|
<div style="margin-top: 10px; text-align: right;">
|
||||||
</el-tab-pane>
|
<el-button type="primary" @click="uploadText" :loading="importing">提交</el-button>
|
||||||
<el-tab-pane label="QA 问答" name="qa">
|
</div>
|
||||||
<div v-for="(qa, index) in qaList" :key="index" style="margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 15px;">
|
</el-tab-pane>
|
||||||
<el-input v-model="qa.question" placeholder="问题 (Question)" style="margin-bottom: 5px;"></el-input>
|
<el-tab-pane label="QA 问答" name="qa">
|
||||||
<el-input v-model="qa.answer" type="textarea" placeholder="回答 (Answer)"></el-input>
|
<div v-for="(qa, index) in qaList" :key="index" style="margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 15px;">
|
||||||
<el-button type="danger" link size="small" @click="removeQA(index)" v-if="qaList.length > 1">删除</el-button>
|
<el-input v-model="qa.question" placeholder="问题 (Question)" style="margin-bottom: 5px;" :disabled="importing"></el-input>
|
||||||
</div>
|
<el-input v-model="qa.answer" type="textarea" placeholder="回答 (Answer)" :disabled="importing"></el-input>
|
||||||
<el-button type="primary" plain style="width: 100%; margin-bottom: 10px;" @click="addQA">添加一组 QA</el-button>
|
<el-button type="danger" link size="small" @click="removeQA(index)" v-if="qaList.length > 1" :disabled="importing">删除</el-button>
|
||||||
<div style="text-align: right;">
|
</div>
|
||||||
<el-button type="primary" @click="uploadQA">提交所有 QA</el-button>
|
<el-button type="primary" plain style="width: 100%; margin-bottom: 10px;" @click="addQA" :disabled="importing">添加一组 QA</el-button>
|
||||||
</div>
|
<div style="text-align: right;">
|
||||||
</el-tab-pane>
|
<el-button type="primary" @click="uploadQA" :loading="importing">提交所有 QA</el-button>
|
||||||
</el-tabs>
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 文档详情弹窗 -->
|
<!-- 文档详情弹窗 -->
|
||||||
|
|
@ -185,6 +188,7 @@
|
||||||
const documents = ref([]);
|
const documents = ref([]);
|
||||||
const loadingDocs = ref(false);
|
const loadingDocs = ref(false);
|
||||||
const showImportDialog = ref(false);
|
const showImportDialog = ref(false);
|
||||||
|
const importing = ref(false);
|
||||||
const importType = ref('file');
|
const importType = ref('file');
|
||||||
const importText = ref('');
|
const importText = ref('');
|
||||||
const qaList = ref([{ question: '', answer: '' }]);
|
const qaList = ref([{ question: '', answer: '' }]);
|
||||||
|
|
@ -208,6 +212,7 @@
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const tokenParam = urlParams.get('token');
|
const tokenParam = urlParams.get('token');
|
||||||
const tenantParam = urlParams.get('tenant_id');
|
const tenantParam = urlParams.get('tenant_id');
|
||||||
|
const isAdmin = ref(false);
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
@ -221,6 +226,7 @@
|
||||||
} else if (tokenParam === 'fzy') {
|
} else if (tokenParam === 'fzy') {
|
||||||
// 管理员模式
|
// 管理员模式
|
||||||
currentPage.value = 'home';
|
currentPage.value = 'home';
|
||||||
|
isAdmin.value = true;
|
||||||
refreshTenants();
|
refreshTenants();
|
||||||
} else {
|
} else {
|
||||||
ElMessage.warning('请在 URL 中提供有效的 token');
|
ElMessage.warning('请在 URL 中提供有效的 token');
|
||||||
|
|
@ -300,6 +306,7 @@
|
||||||
|
|
||||||
// 导入逻辑
|
// 导入逻辑
|
||||||
const uploadFile = async (param) => {
|
const uploadFile = async (param) => {
|
||||||
|
importing.value = true;
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', param.file);
|
formData.append('file', param.file);
|
||||||
try {
|
try {
|
||||||
|
|
@ -309,11 +316,14 @@
|
||||||
fetchDocuments();
|
fetchDocuments();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error('上传失败');
|
ElMessage.error('上传失败');
|
||||||
|
} finally {
|
||||||
|
importing.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadText = async () => {
|
const uploadText = async () => {
|
||||||
if (!importText.value) return;
|
if (!importText.value) return;
|
||||||
|
importing.value = true;
|
||||||
try {
|
try {
|
||||||
await api.post('/ingest/text', { text: importText.value });
|
await api.post('/ingest/text', { text: importText.value });
|
||||||
ElMessage.success('导入成功');
|
ElMessage.success('导入成功');
|
||||||
|
|
@ -322,6 +332,8 @@
|
||||||
fetchDocuments();
|
fetchDocuments();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error('导入失败');
|
ElMessage.error('导入失败');
|
||||||
|
} finally {
|
||||||
|
importing.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -332,6 +344,7 @@
|
||||||
const validQA = qaList.value.filter(q => q.question && q.answer);
|
const validQA = qaList.value.filter(q => q.question && q.answer);
|
||||||
if (validQA.length === 0) return ElMessage.warning('请填写 QA');
|
if (validQA.length === 0) return ElMessage.warning('请填写 QA');
|
||||||
|
|
||||||
|
importing.value = true;
|
||||||
try {
|
try {
|
||||||
await api.post('/ingest/batch_qa', validQA);
|
await api.post('/ingest/batch_qa', validQA);
|
||||||
ElMessage.success(`成功导入 ${validQA.length} 条 QA`);
|
ElMessage.success(`成功导入 ${validQA.length} 条 QA`);
|
||||||
|
|
@ -340,6 +353,8 @@
|
||||||
fetchDocuments();
|
fetchDocuments();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error('导入失败');
|
ElMessage.error('导入失败');
|
||||||
|
} finally {
|
||||||
|
importing.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -447,13 +462,13 @@
|
||||||
return {
|
return {
|
||||||
currentPage, tenants, currentTenantId,
|
currentPage, tenants, currentTenantId,
|
||||||
activeTab, documents, loadingDocs,
|
activeTab, documents, loadingDocs,
|
||||||
showImportDialog, importType, importText, qaList,
|
showImportDialog, importType, importText, qaList, importing,
|
||||||
showDocDialog, currentDocId, currentDocContent, docDetailLoading,
|
showDocDialog, currentDocId, currentDocContent, docDetailLoading,
|
||||||
queryInput, chatHistory, chatLoading, chatBox,
|
queryInput, chatHistory, chatLoading, chatBox,
|
||||||
goHome, refreshTenants, enterTenant, fetchDocuments,
|
goHome, refreshTenants, enterTenant, fetchDocuments,
|
||||||
viewDocument, deleteDocument, deleteCurrentDoc,
|
viewDocument, deleteDocument, deleteCurrentDoc,
|
||||||
uploadFile, uploadText, addQA, removeQA, uploadQA,
|
uploadFile, uploadText, addQA, removeQA, uploadQA,
|
||||||
sendQuery, renderMarkdown, formatDate
|
sendQuery, renderMarkdown, formatDate, isAdmin
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue