仪评指标联分析模块
This commit is contained in:
@@ -4,3 +4,8 @@ __pycache__/
|
||||
|
||||
bbit_ai/test/milvus/milvus_docs/__MACOSX/
|
||||
bbit_ai/test/milvus/milvus_docs/
|
||||
bbit_ai/test/ocr/PP-OCRv5_server_det_infer/
|
||||
bbit_ai/test/ocr/PP-OCRv5_server_rec_infer/
|
||||
vue/vue.tar
|
||||
bbit_ai/test/ocr/
|
||||
bbit_ai/ce-pybackend.tar
|
||||
|
||||
Generated
+1
-1
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<content url="file://$MODULE_DIR$/app" />
|
||||
<orderEntry type="jdk" jdkName="bbit_ai_lab" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
|
||||
Generated
+3
-1
@@ -1,7 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.12" />
|
||||
<option name="enabledOnReformat" value="true" />
|
||||
<option name="enabledOnSave" value="true" />
|
||||
<option name="sdkName" value="bbit_ai_lab" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="bbit_ai_lab" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
@@ -0,0 +1,60 @@
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from langchain_core.messages import HumanMessage
|
||||
|
||||
from config.llm import *
|
||||
from langchain.prompts import PromptTemplate
|
||||
|
||||
from langchain.prompts import PromptTemplate
|
||||
from langchain.schema import HumanMessage
|
||||
import os
|
||||
import base64
|
||||
from langchain.prompts import PromptTemplate
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langchain_core.output_parsers import JsonOutputParser
|
||||
from langchain.schema import HumanMessage
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
import json
|
||||
|
||||
import re
|
||||
import json
|
||||
import requests
|
||||
import cv2
|
||||
import numpy as np
|
||||
import requests
|
||||
|
||||
class CocoonSample(BaseModel):
|
||||
moisture_content: float = Field(
|
||||
...,
|
||||
description="茧的含水量,单位为百分比(%),浮点数"
|
||||
)
|
||||
cocoon_weight: float = Field(
|
||||
...,
|
||||
description="下足茧的重量,单位为克,可带小数"
|
||||
)
|
||||
defective_pupa_count: int = Field(
|
||||
...,
|
||||
description="非好蛹粒数,即不合格蛹的数量,整数"
|
||||
)
|
||||
fresh_shell_weight: float = Field(
|
||||
...,
|
||||
description="鲜壳重量,单位为克,可带小数"
|
||||
)
|
||||
sample_count: int = Field(
|
||||
...,
|
||||
description="小样粒数,用于检测的茧粒数,整数"
|
||||
)
|
||||
net_weight_total: float = Field(
|
||||
...,
|
||||
description="所有样品的净重合计,单位为克,浮点数"
|
||||
)
|
||||
evaluator: Optional[str] = Field(
|
||||
None,
|
||||
description="仪评人姓名,可能为空"
|
||||
)
|
||||
reviewer: Optional[str] = Field(
|
||||
None,
|
||||
description="复核人员姓名,可能为空"
|
||||
)
|
||||
+20
-12
@@ -1,18 +1,22 @@
|
||||
from fastapi import FastAPI
|
||||
from routers.Chat import chatRouter
|
||||
from routers.Report import reportRouter
|
||||
from routers.Datasource import reportDataRouter
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from routers.Knowledge import knowledgeRouter
|
||||
from routers.Service import serviceRouter
|
||||
|
||||
from routers.Bot import botRouter
|
||||
from routers.Chat import chatRouter
|
||||
from routers.Datasource import reportDataRouter
|
||||
from routers.F8 import f8Router
|
||||
from routers.Knowledge import knowledgeRouter
|
||||
from routers.Report import reportRouter
|
||||
from routers.Service import serviceRouter
|
||||
from routers.Vision import visionRouter
|
||||
|
||||
app = FastAPI(title="BBIT_AI")
|
||||
|
||||
origins = [
|
||||
"http://localhost:8090", # Vite dev 默认端口
|
||||
"http://127.0.0.1:5173",
|
||||
"http://s1.ronsunny.cn:8089",
|
||||
"*" # ⚠️ 生产环境不要用
|
||||
"*", # ⚠️ 生产环境不要用
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
@@ -22,9 +26,13 @@ app.add_middleware(
|
||||
allow_methods=["*"], # 必须包含 OPTIONS、GET 等
|
||||
allow_headers=["*"],
|
||||
)
|
||||
app.include_router(chatRouter, prefix="/api/llm", tags=["chat"])
|
||||
app.include_router(reportRouter, prefix="/api/llm", tags=["chat"])
|
||||
app.include_router(knowledgeRouter, prefix="/api/llm", tags=["chat"])
|
||||
app.include_router(reportDataRouter, prefix="/api/llm", tags=["chat"])
|
||||
app.include_router(serviceRouter, prefix="/api/llm", tags=["chat"])
|
||||
app.include_router(botRouter, prefix="/api/llm", tags=["chat"])
|
||||
app.include_router(chatRouter, prefix="/api/llm", tags=["llm"])
|
||||
app.include_router(reportRouter, prefix="/api/llm", tags=["llm"])
|
||||
app.include_router(knowledgeRouter, prefix="/api/llm", tags=["llm"])
|
||||
app.include_router(reportDataRouter, prefix="/api/llm", tags=["llm"])
|
||||
app.include_router(serviceRouter, prefix="/api/llm", tags=["llm"])
|
||||
app.include_router(botRouter, prefix="/api/llm", tags=["llm"])
|
||||
app.include_router(visionRouter, prefix="/api/llm", tags=["llm"])
|
||||
|
||||
|
||||
app.include_router(f8Router, prefix="/api/f8", tags=["f8"])
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
SERVER_PATH_OSS = "s1.ronsunny.cn"
|
||||
|
||||
F8_SERVER_USER_ID = "da33efb9-776a-443b-b1ec-dbbbf08793d7"
|
||||
@@ -6,6 +6,8 @@ from langchain_openai import ChatOpenAI
|
||||
from openai import OpenAI
|
||||
import os
|
||||
from langchain_openai import OpenAIEmbeddings
|
||||
from langchain_community.embeddings import DashScopeEmbeddings
|
||||
|
||||
# 通义千文Key
|
||||
tongyiKey = "sk-9464b2498c184982a9fe9d2c2e725ab5"
|
||||
# DeepSeekKey
|
||||
@@ -18,13 +20,16 @@ llmThink = ChatOpenAI(
|
||||
api_key=tongyiKey,
|
||||
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
model="qwen-max",
|
||||
stream = False
|
||||
)
|
||||
from langchain_community.embeddings import DashScopeEmbeddings
|
||||
embeddings = DashScopeEmbeddings(
|
||||
llmEmbeddings = DashScopeEmbeddings(
|
||||
model="text-embedding-v3",
|
||||
dashscope_api_key= tongyiKey,
|
||||
)
|
||||
llmVision = ChatOpenAI(
|
||||
api_key=tongyiKey,
|
||||
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
model="qwen-vl-plus",
|
||||
)
|
||||
|
||||
# from langchain_deepseek import ChatDeepSeek
|
||||
# llm = ChatDeepSeek(
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from langchain_milvus import BM25BuiltInFunction, Milvus
|
||||
from config.llm import embeddings
|
||||
from config.llm import llmEmbeddings
|
||||
|
||||
URI = "http://10.10.10.9:19530"
|
||||
|
||||
knVectorstore = Milvus(
|
||||
embedding_function=embeddings,
|
||||
embedding_function=llmEmbeddings,
|
||||
connection_args={"uri": URI, "token": "root:Milvus", "db_name": "bbit_ai_lab"},
|
||||
collection_name="knowledge",
|
||||
index_params={"index_type": "FLAT", "metric_type": "L2"},
|
||||
@@ -19,7 +19,7 @@ knVectorstore = Milvus(
|
||||
drop_old=False, # set to True if seeking to drop the collection with that name if it exists
|
||||
)
|
||||
memVectorstore = Milvus(
|
||||
embedding_function=embeddings,
|
||||
embedding_function=llmEmbeddings,
|
||||
connection_args={"uri": URI, "token": "root:Milvus", "db_name": "bbit_ai_lab"},
|
||||
collection_name="memory",
|
||||
index_params={"index_type": "FLAT", "metric_type": "L2"},
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from minio import Minio
|
||||
|
||||
# MinIO 客户端初始化
|
||||
minio_client = Minio(
|
||||
"s1.ronsunny.cn:9000",
|
||||
access_key="minioadmin",
|
||||
secret_key="minioadmin",
|
||||
secure=False,
|
||||
)
|
||||
|
||||
|
||||
def push_file(bucket_name, object_name, file_bytes, contents, content_type):
|
||||
minio_client.put_object(
|
||||
bucket_name,
|
||||
object_name,
|
||||
file_bytes,
|
||||
length=len(contents),
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
|
||||
def get_temp_url(bucket_name, object_name):
|
||||
return minio_client.presigned_get_object(
|
||||
bucket_name, object_name, expires=timedelta(seconds=3600)
|
||||
)
|
||||
+240
-90
@@ -1,8 +1,9 @@
|
||||
from langchain_postgres import PostgresChatMessageHistory
|
||||
from config.pgDb import pg_pool
|
||||
from config.ssDb import mssql_pool
|
||||
from typing import List, Dict
|
||||
import json
|
||||
from typing import List, Dict
|
||||
|
||||
from langchain_postgres import PostgresChatMessageHistory
|
||||
|
||||
from config.pgDb import pg_pool
|
||||
|
||||
# ————————————————————————————————————————————————————AI角色———————————————————————————————
|
||||
|
||||
@@ -13,8 +14,7 @@ def get_ai_personality(ai_id: str):
|
||||
with pg_pool.getConn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT ai_personality FROM ai_chat_profiles WHERE id = %s",
|
||||
(ai_id,)
|
||||
"SELECT ai_personality FROM ai_chat_profiles WHERE id = %s", (ai_id,)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
@@ -27,8 +27,7 @@ def get_description(ai_id: str):
|
||||
with pg_pool.getConn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT description FROM ai_chat_profiles WHERE id = %s",
|
||||
(ai_id,)
|
||||
"SELECT description FROM ai_chat_profiles WHERE id = %s", (ai_id,)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
@@ -40,8 +39,7 @@ def get_description(ai_id: str):
|
||||
def get_ai_available_kn_bases(ai_id: str) -> List[str]:
|
||||
with pg_pool.getConn() as conn:
|
||||
result = conn.execute(
|
||||
"SELECT available_kn_bases FROM ai_chat_profiles WHERE id = %s",
|
||||
(ai_id,)
|
||||
"SELECT available_kn_bases FROM ai_chat_profiles WHERE id = %s", (ai_id,)
|
||||
)
|
||||
return result.fetchone()[0]
|
||||
|
||||
@@ -50,10 +48,7 @@ def get_all_ai_bot(user_id: str, module: str) -> List[Dict]:
|
||||
with pg_pool.getConn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
# 查询用户角色
|
||||
cur.execute(
|
||||
"SELECT roles FROM users WHERE id = %s",
|
||||
(user_id,)
|
||||
)
|
||||
cur.execute("SELECT roles FROM users WHERE id = %s", (user_id,))
|
||||
role_row = cur.fetchone()
|
||||
if not role_row:
|
||||
return [] # 用户不存在
|
||||
@@ -69,27 +64,37 @@ def get_all_ai_bot(user_id: str, module: str) -> List[Dict]:
|
||||
AND is_active = TRUE
|
||||
AND available_roles::jsonb ?| %s
|
||||
""",
|
||||
(module, user_roles)
|
||||
(module, user_roles),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
result = []
|
||||
for row in rows:
|
||||
# row 索引对应 SELECT 字段顺序
|
||||
id_, title, description, welcome_words, ai_personality, available_report_tables, available_kn_bases = row
|
||||
(
|
||||
id_,
|
||||
title,
|
||||
description,
|
||||
welcome_words,
|
||||
ai_personality,
|
||||
available_report_tables,
|
||||
available_kn_bases,
|
||||
) = row
|
||||
|
||||
# 解析 JSON
|
||||
roles_json = ai_personality if ai_personality else {}
|
||||
result.append({
|
||||
"id": id_,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"welcome_words": welcome_words,
|
||||
"name": roles_json.get("名字", ""),
|
||||
"role": roles_json.get("性格", ""),
|
||||
"service": roles_json.get("业务", ""),
|
||||
"available_report_tables": available_report_tables,
|
||||
"available_kn_bases": available_kn_bases
|
||||
})
|
||||
result.append(
|
||||
{
|
||||
"id": id_,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"welcome_words": welcome_words,
|
||||
"name": roles_json.get("名字", ""),
|
||||
"role": roles_json.get("性格", ""),
|
||||
"service": roles_json.get("业务", ""),
|
||||
"available_report_tables": available_report_tables,
|
||||
"available_kn_bases": available_kn_bases,
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -98,9 +103,7 @@ def get_all_ai_bot(user_id: str, module: str) -> List[Dict]:
|
||||
def insert_message(session_id: str, isAI: bool, content: str):
|
||||
with pg_pool.getConn() as conn:
|
||||
history = PostgresChatMessageHistory(
|
||||
database_name,
|
||||
session_id,
|
||||
sync_connection=conn
|
||||
database_name, session_id, sync_connection=conn
|
||||
)
|
||||
if isAI:
|
||||
history.add_ai_message(content)
|
||||
@@ -112,15 +115,10 @@ def get_history(session_id: str):
|
||||
simplified = []
|
||||
with pg_pool.getConn() as conn:
|
||||
history = PostgresChatMessageHistory(
|
||||
database_name,
|
||||
session_id,
|
||||
sync_connection=conn
|
||||
database_name, session_id, sync_connection=conn
|
||||
)
|
||||
for msg in history.messages:
|
||||
simplified.append({
|
||||
"type": msg.type,
|
||||
"content": msg.content
|
||||
})
|
||||
simplified.append({"type": msg.type, "content": msg.content})
|
||||
return simplified
|
||||
|
||||
|
||||
@@ -129,28 +127,33 @@ def get_history_with_time(session_id: str, number: int):
|
||||
with pg_pool.getConn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
f"SELECT message, created_at FROM ai_chat_history WHERE session_id = '{session_id}' ORDER BY created_at DESC LIMIT {number}")
|
||||
f"SELECT message, created_at FROM ai_chat_history WHERE session_id = '{session_id}' ORDER BY created_at DESC LIMIT {number}"
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
simplified = []
|
||||
|
||||
for row in rows:
|
||||
msg_dict = row[0]
|
||||
simplified.append({
|
||||
"type": msg_dict.get("type"),
|
||||
"created_at": row[1].isoformat(),
|
||||
"content": msg_dict.get("data", {}).get("content")
|
||||
})
|
||||
simplified.append(
|
||||
{
|
||||
"type": msg_dict.get("type"),
|
||||
"created_at": row[1].isoformat(),
|
||||
"content": msg_dict.get("data", {}).get("content"),
|
||||
}
|
||||
)
|
||||
|
||||
return simplified
|
||||
|
||||
|
||||
# ————————————————————————————————————————————————————会话———————————————————————————————
|
||||
def insert_session(user_id: str, ai_id: str, session_id: str, session_title: str, available_module):
|
||||
def insert_session(
|
||||
user_id: str, ai_id: str, session_id: str, session_title: str, available_module
|
||||
):
|
||||
with pg_pool.getConn() as coon:
|
||||
with coon.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO ai_chat_sessions (id ,user_id, ai_id, title, available_module, created_at, updated_at) VALUES (%s, %s, %s, %s,%s, NOW(), NOW())",
|
||||
(session_id, user_id, ai_id, session_title, available_module)
|
||||
(session_id, user_id, ai_id, session_title, available_module),
|
||||
)
|
||||
coon.commit()
|
||||
|
||||
@@ -160,7 +163,7 @@ def update_session_updated_at(session_id: str):
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE ai_chat_sessions SET updated_at = NOW() WHERE id = %s",
|
||||
(session_id,)
|
||||
(session_id,),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
@@ -173,16 +176,12 @@ def get_sessions(user_id: str, available_module: str):
|
||||
"FROM ai_chat_sessions "
|
||||
"WHERE user_id = %s AND available_module = %s "
|
||||
"ORDER BY updated_at DESC",
|
||||
(user_id, available_module)
|
||||
(user_id, available_module),
|
||||
)
|
||||
|
||||
sessions = cur.fetchall()
|
||||
return [
|
||||
{
|
||||
"id": row[0],
|
||||
"title": row[1],
|
||||
"updated_at": row[2]
|
||||
}
|
||||
{"id": row[0], "title": row[1], "updated_at": row[2]}
|
||||
for row in sessions
|
||||
]
|
||||
|
||||
@@ -193,16 +192,10 @@ def get_reports(user_id: str):
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT id, title FROM ai_reports WHERE created_by = %s AND is_masked = TRUE ORDER BY created_at DESC",
|
||||
(user_id,)
|
||||
(user_id,),
|
||||
)
|
||||
reports = cur.fetchall()
|
||||
return [
|
||||
{
|
||||
"id": row[0],
|
||||
"title": row[1]
|
||||
}
|
||||
for row in reports
|
||||
]
|
||||
return [{"id": row[0], "title": row[1]} for row in reports]
|
||||
|
||||
|
||||
def save_report(id: str, user_id: str, title: str, sql: str):
|
||||
@@ -210,7 +203,7 @@ def save_report(id: str, user_id: str, title: str, sql: str):
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO ai_reports (id, title, sql, created_at, created_by , is_masked) VALUES (%s, %s, %s, NOW(), %s, FALSE) RETURNING id",
|
||||
(id, title, sql, user_id)
|
||||
(id, title, sql, user_id),
|
||||
)
|
||||
report_id = cur.fetchone()[0]
|
||||
conn.commit()
|
||||
@@ -222,7 +215,7 @@ def maked_report(report_id: str, title: str):
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE ai_reports SET title = %s, is_masked = TRUE WHERE id = %s",
|
||||
(title, report_id)
|
||||
(title, report_id),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
@@ -230,10 +223,7 @@ def maked_report(report_id: str, title: str):
|
||||
def getSQL(reportId: str):
|
||||
with pg_pool.getConn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT sql FROM ai_reports WHERE id = %s",
|
||||
(reportId,)
|
||||
)
|
||||
cur.execute("SELECT sql FROM ai_reports WHERE id = %s", (reportId,))
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
return row[0]
|
||||
@@ -247,7 +237,7 @@ def get_available_tables_str(aiId: str):
|
||||
# 1. 先取 AI 可用的数据库表
|
||||
cur.execute(
|
||||
"SELECT available_report_tables FROM ai_chat_profiles WHERE id = %s",
|
||||
(aiId,)
|
||||
(aiId,),
|
||||
)
|
||||
role_row = cur.fetchone()
|
||||
if not role_row:
|
||||
@@ -258,7 +248,7 @@ def get_available_tables_str(aiId: str):
|
||||
return "无数据库表可用"
|
||||
|
||||
# 2. 构造 IN 查询占位符
|
||||
placeholders = ','.join(['%s'] * len(available_tables))
|
||||
placeholders = ",".join(["%s"] * len(available_tables))
|
||||
sql_query = f"""
|
||||
SELECT id, name, description
|
||||
FROM ai_reports_tables
|
||||
@@ -272,7 +262,7 @@ def get_available_tables_str(aiId: str):
|
||||
for table in tableIds:
|
||||
cur.execute(
|
||||
"SELECT name, type, description FROM ai_reports_fields WHERE table_id = %s AND is_active = TRUE",
|
||||
(table[0],)
|
||||
(table[0],),
|
||||
)
|
||||
columns = cur.fetchall()
|
||||
result += f"{table[1]}:{table[2]}\n"
|
||||
@@ -291,8 +281,15 @@ def get_available_tables():
|
||||
cursor.execute(
|
||||
"SELECT id, name, description,is_active FROM ai_reports_tables",
|
||||
)
|
||||
return [{"id": row[0], "name": row[1], "description": row[2], "is_active": row[3]} for row in
|
||||
cursor.fetchall()]
|
||||
return [
|
||||
{
|
||||
"id": row[0],
|
||||
"name": row[1],
|
||||
"description": row[2],
|
||||
"is_active": row[3],
|
||||
}
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
|
||||
|
||||
# 新增表
|
||||
@@ -305,7 +302,7 @@ def add_table(name, description, user_id):
|
||||
VALUES (%s, %s, %s)
|
||||
RETURNING id
|
||||
""",
|
||||
(name, description, user_id)
|
||||
(name, description, user_id),
|
||||
)
|
||||
new_id = cursor.fetchone()[0] # 取返回的 id
|
||||
return new_id
|
||||
@@ -319,8 +316,16 @@ def get_fields_by_table_id(table_id):
|
||||
"SELECT id, name, type, description, is_active FROM ai_reports_fields WHERE table_id = %s",
|
||||
(table_id,),
|
||||
)
|
||||
return [{"id": row[0], "name": row[1], "type": row[2], "description": row[3], "is_active": row[4]} for row
|
||||
in cursor.fetchall()]
|
||||
return [
|
||||
{
|
||||
"id": row[0],
|
||||
"name": row[1],
|
||||
"type": row[2],
|
||||
"description": row[3],
|
||||
"is_active": row[4],
|
||||
}
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
|
||||
|
||||
# 新增字段
|
||||
@@ -329,18 +334,26 @@ def add_field(name, type, description, is_active, table_id, user_id):
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"INSERT INTO ai_reports_fields (name,type,description, is_active, create_by, table_id) VALUES (%s, %s, %s, %s, %s, %s) RETURNING id",
|
||||
(name, type, description, is_active, user_id, table_id)
|
||||
(name, type, description, is_active, user_id, table_id),
|
||||
)
|
||||
new_id = cursor.fetchone()[0] # 取返回的 id
|
||||
return new_id
|
||||
|
||||
|
||||
# 新增报表智能体
|
||||
def insert_bot(title: str, description: str, welcome_words: str, ai_personality: str, available_module: str,
|
||||
available_report_tables: str, available_kn_bases: str, user_id: str):
|
||||
def insert_bot(
|
||||
title: str,
|
||||
description: str,
|
||||
welcome_words: str,
|
||||
ai_personality: str,
|
||||
available_module: str,
|
||||
available_report_tables: str,
|
||||
available_kn_bases: str,
|
||||
user_id: str,
|
||||
):
|
||||
with pg_pool.getConn() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
available_roles = json.dumps(['user'])
|
||||
available_roles = json.dumps(["user"])
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO ai_chat_profiles
|
||||
@@ -348,19 +361,38 @@ def insert_bot(title: str, description: str, welcome_words: str, ai_personality:
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, now())
|
||||
RETURNING id
|
||||
""",
|
||||
(available_module, available_roles, title, description, welcome_words, ai_personality,
|
||||
available_report_tables, available_kn_bases, user_id)
|
||||
(
|
||||
available_module,
|
||||
available_roles,
|
||||
title,
|
||||
description,
|
||||
welcome_words,
|
||||
ai_personality,
|
||||
available_report_tables,
|
||||
available_kn_bases,
|
||||
user_id,
|
||||
),
|
||||
)
|
||||
report_id = cursor.fetchone()[0]
|
||||
return report_id
|
||||
|
||||
|
||||
# 更新报表智能体
|
||||
def update_bot(id: str, title: str, description: str, welcome_words: str, ai_personality: str, available_module: str,
|
||||
available_report_tables: str, available_kn_bases: str, user_id: str):
|
||||
def update_bot(
|
||||
id: str,
|
||||
title: str,
|
||||
description: str,
|
||||
welcome_words: str,
|
||||
ai_personality: str,
|
||||
available_module: str,
|
||||
available_report_tables: str,
|
||||
available_kn_bases: str,
|
||||
user_id: str,
|
||||
):
|
||||
with pg_pool.getConn() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE ai_chat_profiles
|
||||
SET title = %s,
|
||||
description = %s,
|
||||
@@ -373,9 +405,18 @@ def update_bot(id: str, title: str, description: str, welcome_words: str, ai_per
|
||||
updated_by = %s
|
||||
WHERE id = %s
|
||||
""",
|
||||
(title, description, ai_personality, welcome_words, available_report_tables,
|
||||
available_kn_bases, available_module, user_id, id)
|
||||
)
|
||||
(
|
||||
title,
|
||||
description,
|
||||
ai_personality,
|
||||
welcome_words,
|
||||
available_report_tables,
|
||||
available_kn_bases,
|
||||
available_module,
|
||||
user_id,
|
||||
id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ————————————————————————————————————————————————————知识库———————————————————————————————
|
||||
@@ -388,11 +429,18 @@ def get_available_knowledge_bases(available_module: str):
|
||||
FROM ai_knowledge
|
||||
WHERE available_module::jsonb @> %s::jsonb
|
||||
""",
|
||||
(f'["{available_module}"]',)
|
||||
(f'["{available_module}"]',),
|
||||
)
|
||||
|
||||
return [{"id": row[0], "name": row[1], "description": row[2], "is_active": row[3]} for row in
|
||||
cursor.fetchall()]
|
||||
return [
|
||||
{
|
||||
"id": row[0],
|
||||
"name": row[1],
|
||||
"description": row[2],
|
||||
"is_active": row[3],
|
||||
}
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
|
||||
|
||||
def add_knowledge_base(name, description, user_id):
|
||||
@@ -404,6 +452,108 @@ def add_knowledge_base(name, description, user_id):
|
||||
VALUES (%s, %s, %s, now())
|
||||
RETURNING id
|
||||
""",
|
||||
(name, description, user_id)
|
||||
(name, description, user_id),
|
||||
)
|
||||
new_id = cursor.fetchone()[0] # 取返回的 id
|
||||
return new_id
|
||||
|
||||
|
||||
# ————————————————————————————————————————————————————仪评指标联识别———————————————————————————————
|
||||
from config.minIO import get_temp_url
|
||||
import utils.MyUtils as MyUtils
|
||||
|
||||
|
||||
def get_ticket_image_list(user_id):
|
||||
with pg_pool.getConn() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT created_at, file_name, resolution, size, name,
|
||||
moisture_content, cocoon_weight, defective_pupa_count,
|
||||
fresh_shell_weight, sample_count, barcode, oss,
|
||||
net_weight_total, evaluator, reviewer,id
|
||||
FROM ticket_images
|
||||
WHERE created_by = %s
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
result = []
|
||||
for row in rows:
|
||||
result.append(
|
||||
{
|
||||
"created_at": MyUtils.format_datetime(row[0]),
|
||||
"file_name": row[1],
|
||||
"resolution": row[2],
|
||||
"size": round(row[3], 2),
|
||||
"name": row[4],
|
||||
"moisture_content": row[5],
|
||||
"cocoon_weight": row[6],
|
||||
"defective_pupa_count": row[7],
|
||||
"fresh_shell_weight": row[8],
|
||||
"sample_count": row[9],
|
||||
"barcode": row[10],
|
||||
# "oss_url": f"http://{SERVER_PATH_OSS}:9000/image-ticket/{row[11]}",
|
||||
"oss_url": get_temp_url("image-ticket", row[11]),
|
||||
"net_weight_total": row[12],
|
||||
"evaluator": row[13],
|
||||
"reviewer": row[14],
|
||||
"id": row[15],
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def insert_ticket_image(
|
||||
created_by,
|
||||
file_name,
|
||||
resolution,
|
||||
size,
|
||||
name,
|
||||
moisture_content,
|
||||
cocoon_weight,
|
||||
defective_pupa_count,
|
||||
fresh_shell_weight,
|
||||
sample_count,
|
||||
barcode,
|
||||
oss,
|
||||
net_weight_total,
|
||||
evaluator,
|
||||
reviewer,
|
||||
):
|
||||
with pg_pool.getConn() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO ticket_images (
|
||||
created_by, file_name, resolution, size, name,
|
||||
moisture_content, cocoon_weight, defective_pupa_count,
|
||||
fresh_shell_weight, sample_count, barcode, oss,
|
||||
net_weight_total, evaluator, reviewer, created_at
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s,
|
||||
%s, %s, %s, %s, %s,
|
||||
%s, %s, %s, %s, %s, NOW())
|
||||
RETURNING id
|
||||
""",
|
||||
(
|
||||
created_by,
|
||||
file_name,
|
||||
resolution,
|
||||
size,
|
||||
name,
|
||||
moisture_content,
|
||||
cocoon_weight,
|
||||
defective_pupa_count,
|
||||
fresh_shell_weight,
|
||||
sample_count,
|
||||
barcode,
|
||||
oss,
|
||||
net_weight_total,
|
||||
evaluator,
|
||||
reviewer,
|
||||
),
|
||||
)
|
||||
new_id = cursor.fetchone()[0]
|
||||
conn.commit()
|
||||
return new_id
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
from langchain.schema import HumanMessage
|
||||
|
||||
from config.llm import *
|
||||
|
||||
|
||||
def get_ticket_response(image_url: str):
|
||||
# 构建 prompt
|
||||
prompt_text = f"""
|
||||
你是一位专业的图像分析AI。你的任务是仔细分析提供的图片内容,并按JSON格式输出结果。
|
||||
|
||||
## JSON输出结构及字段说明:
|
||||
# 蚕茧检测信息数据模型(ImageDescription)
|
||||
|
||||
该模型用于描述蚕茧检测信息,每条记录包含以下字段:
|
||||
|
||||
## 1. 含水率 (`moisture_content`)
|
||||
- **类型**:浮点数
|
||||
- **描述**:茧的含水量,单位为百分比(%)
|
||||
|
||||
## 2. 下足茧重量 (`cocoon_weight`)
|
||||
- **类型**:浮点数
|
||||
- **描述**:下足茧的重量,单位为克(可带小数)
|
||||
|
||||
## 3. 非好蛹粒数 (`defective_pupa_count`)
|
||||
- **类型**:整数
|
||||
- **描述**:不合格蛹的数量,即非好蛹的个数
|
||||
|
||||
## 4. 鲜壳量 (`fresh_shell_weight`)
|
||||
- **类型**:浮点数
|
||||
- **描述**:鲜壳重量,单位为克(可带小数)
|
||||
|
||||
## 5. 小样粒数 (`sample_count`)
|
||||
- **类型**:整数
|
||||
- **描述**:检测使用的小样数量,即用于检测的茧粒数
|
||||
|
||||
## 6. 净重合计 (`net_weight_total`)
|
||||
- **类型**:浮点数
|
||||
- **描述**:所有样品的净重总和,单位为克
|
||||
|
||||
## 7. 仪评人姓名 (`evaluator`)
|
||||
- **类型**:字符串
|
||||
- **描述**:进行仪器检测的人员姓名,可能为空
|
||||
|
||||
## 8. 复核人员姓名 (`reviewer`)
|
||||
- **类型**:字符串
|
||||
- **描述**:复核人员姓名,可能为空
|
||||
|
||||
---
|
||||
|
||||
### 注意事项
|
||||
- 所有字段都是必填的(required),在 JSON 实例中必须提供值
|
||||
- 浮点数字段可以包含小数,整数字段只能是整数
|
||||
- `evaluator` 和 `reviewer` 可以为空字符串,但字段必须存在
|
||||
|
||||
最后,只输出严格的 JSON 格式,不要包含其他文字、markdown等内容。
|
||||
"""
|
||||
messages = [
|
||||
HumanMessage(
|
||||
content=[
|
||||
{"type": "text", "text": prompt_text},
|
||||
{"type": "image_url", "image_url": {"url": image_url}},
|
||||
]
|
||||
)
|
||||
]
|
||||
# 直接调用模型
|
||||
llmRes = llmVision.invoke(messages).content
|
||||
# 去掉 ```json 和 ``` 包裹
|
||||
cleaned = re.sub(r"^```json\s*|\s*```$", "", llmRes.strip())
|
||||
# 解析 JSON
|
||||
jsonRes = json.loads(cleaned)
|
||||
jsonRes["barcode"] = decode_barcode(image_url)
|
||||
return jsonRes
|
||||
|
||||
|
||||
import os
|
||||
import base64
|
||||
import tempfile
|
||||
import requests
|
||||
from pyzxing import BarCodeReader
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
from fastapi import UploadFile
|
||||
|
||||
|
||||
def decode_barcode(input_data) -> str:
|
||||
"""
|
||||
自动识别输入类型并解析条码:
|
||||
- URL 字符串
|
||||
- Base64 字符串
|
||||
- UploadFile / bytes / 文件对象
|
||||
返回第一个条码的内容,解析失败返回空字符串
|
||||
"""
|
||||
# ---------------- 输入处理 ----------------
|
||||
img = None
|
||||
|
||||
# URL
|
||||
if isinstance(input_data, str) and (
|
||||
input_data.startswith("http://") or input_data.startswith("https://")
|
||||
):
|
||||
response = requests.get(input_data)
|
||||
response.raise_for_status()
|
||||
img = Image.open(BytesIO(response.content))
|
||||
|
||||
# Base64 字符串
|
||||
elif isinstance(input_data, str):
|
||||
# 过滤 data URI 前缀
|
||||
if "," in input_data:
|
||||
input_data = input_data.split(",")[1]
|
||||
try:
|
||||
img_data = base64.b64decode(input_data)
|
||||
img = Image.open(BytesIO(img_data))
|
||||
except Exception:
|
||||
raise ValueError("无法解析 Base64 字符串")
|
||||
|
||||
# UploadFile / bytes / 文件对象
|
||||
elif isinstance(input_data, UploadFile):
|
||||
content = input_data.file.read()
|
||||
input_data.file.seek(0)
|
||||
img = Image.open(BytesIO(content))
|
||||
elif isinstance(input_data, bytes):
|
||||
img = Image.open(BytesIO(input_data))
|
||||
elif hasattr(input_data, "read"): # 文件对象
|
||||
content = input_data.read()
|
||||
if hasattr(input_data, "seek"):
|
||||
input_data.seek(0)
|
||||
img = Image.open(BytesIO(content))
|
||||
else:
|
||||
raise ValueError("不支持的输入类型")
|
||||
|
||||
# ---------------- 临时文件处理 ----------------
|
||||
tmp_fd, tmp_path = tempfile.mkstemp(suffix=".png")
|
||||
try:
|
||||
with open(tmp_path, "wb") as f:
|
||||
img.save(f, format="PNG")
|
||||
|
||||
# ---------------- 调用 pyzxing ----------------
|
||||
reader = BarCodeReader()
|
||||
result = reader.decode(tmp_path)
|
||||
|
||||
if result:
|
||||
parsed = result[0].get("parsed", "")
|
||||
if isinstance(parsed, bytes):
|
||||
parsed = parsed.decode("utf-8")
|
||||
return parsed
|
||||
return ""
|
||||
finally:
|
||||
os.close(tmp_fd)
|
||||
if os.path.exists(tmp_path):
|
||||
os.remove(tmp_path)
|
||||
@@ -0,0 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class F8ImageRequest(BaseModel):
|
||||
title: str
|
||||
image: str
|
||||
@@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ImageRequest(BaseModel):
|
||||
image: str
|
||||
@@ -0,0 +1,30 @@
|
||||
import base64
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from config.app import F8_SERVER_USER_ID
|
||||
from models.BaseResponse import BaseResponse
|
||||
from models.F8ImageRequest import F8ImageRequest
|
||||
from service.vision import process_ticket_image
|
||||
from utils import MyUtils
|
||||
|
||||
f8Router = APIRouter()
|
||||
|
||||
|
||||
@f8Router.post("/createTicketImageTask")
|
||||
async def cocoonTicket(data: F8ImageRequest):
|
||||
input_data = data.image
|
||||
if "," in input_data:
|
||||
input_data = input_data.split(",")[1]
|
||||
try:
|
||||
img_bytes = base64.b64decode(input_data)
|
||||
json_data = await MyUtils.async_task(
|
||||
process_ticket_image,
|
||||
img_bytes,
|
||||
f"{data.title}.jpg",
|
||||
data.title,
|
||||
F8_SERVER_USER_ID,
|
||||
)
|
||||
return BaseResponse(data=json_data)
|
||||
except Exception as e:
|
||||
return BaseResponse(status=False, message=f"解析失败: {str(e)}", data=None)
|
||||
@@ -0,0 +1,51 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, File, Form, Depends
|
||||
|
||||
import db.postgres as pg
|
||||
from config.security import get_user_id_from_token
|
||||
from llm.ticketLLM import *
|
||||
from models.BaseResponse import BaseResponse
|
||||
from models.ImageRequest import ImageRequest
|
||||
from service.vision import process_ticket_image
|
||||
from utils import MyUtils
|
||||
|
||||
visionRouter = APIRouter()
|
||||
|
||||
|
||||
# 测试接口,已经可以实现
|
||||
@visionRouter.post("/cocoonTicket")
|
||||
def cocoonTicket(data: ImageRequest, user_id: UUID = Depends(get_user_id_from_token)):
|
||||
if not user_id:
|
||||
return {"error": "userId is required"}
|
||||
try:
|
||||
json = get_ticket_response(data.image)
|
||||
return BaseResponse(data=json)
|
||||
except:
|
||||
return BaseResponse(status=False, message="unknown error", data=None)
|
||||
|
||||
|
||||
@visionRouter.post("/createTicketImageTask")
|
||||
async def createTicketImageTask(
|
||||
file: UploadFile = File(...),
|
||||
projectName: str = Form(...),
|
||||
user_id: UUID = Depends(get_user_id_from_token),
|
||||
):
|
||||
if not user_id:
|
||||
return {"error": "userId is required"}
|
||||
try:
|
||||
contents = await file.read()
|
||||
json_data = await MyUtils.async_task(
|
||||
process_ticket_image, contents, file.filename, projectName, user_id
|
||||
)
|
||||
return BaseResponse(data=json_data)
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
return BaseResponse(status=False, message=f"解析失败: {str(e)}", data=None)
|
||||
|
||||
|
||||
@visionRouter.get("/getTicketImageList")
|
||||
def cocoonTicket(user_id: UUID = Depends(get_user_id_from_token)):
|
||||
if not user_id:
|
||||
return {"error": "userId is required"}
|
||||
return BaseResponse(data=pg.get_ticket_image_list(user_id))
|
||||
@@ -0,0 +1,60 @@
|
||||
import uuid
|
||||
from uuid import UUID
|
||||
|
||||
import config.minIO as minIO
|
||||
import db.postgres as pg
|
||||
from config.minIO import minio_client
|
||||
from llm.ticketLLM import *
|
||||
|
||||
|
||||
def process_ticket_image(
|
||||
img_bytes: bytes,
|
||||
file_name: str = None,
|
||||
project_name: str = None,
|
||||
user_id: UUID = None,
|
||||
):
|
||||
"""
|
||||
处理票据图片的核心逻辑,供不同接口调用
|
||||
"""
|
||||
|
||||
# 上传到 OSS,使用 UUID 做对象名
|
||||
object_name = str(uuid.uuid4())
|
||||
file_bytes = BytesIO(img_bytes)
|
||||
bucket_name = "image-ticket"
|
||||
if not minio_client.bucket_exists(bucket_name):
|
||||
minio_client.make_bucket(bucket_name)
|
||||
|
||||
minIO.push_file(bucket_name, object_name, file_bytes, img_bytes, "image/jpeg")
|
||||
oss_url = minIO.get_temp_url(bucket_name, object_name)
|
||||
|
||||
# 调用分析方法获取 JSON
|
||||
json_data = get_ticket_response(oss_url)
|
||||
# 解析条码
|
||||
barcode = decode_barcode(BytesIO(img_bytes))
|
||||
json_data["barcode"] = barcode
|
||||
|
||||
# 获取图片分辨率和大小
|
||||
img = Image.open(BytesIO(img_bytes))
|
||||
resolution = f"{img.width}x{img.height}"
|
||||
size_kb = len(img_bytes) / 1024
|
||||
|
||||
# 插入数据库
|
||||
pg.insert_ticket_image(
|
||||
created_by=user_id,
|
||||
file_name=file_name,
|
||||
resolution=resolution,
|
||||
size=size_kb,
|
||||
name=project_name if project_name else object_name[:8],
|
||||
moisture_content=json_data.get("moisture_content"),
|
||||
cocoon_weight=json_data.get("cocoon_weight"),
|
||||
defective_pupa_count=json_data.get("defective_pupa_count"),
|
||||
fresh_shell_weight=json_data.get("fresh_shell_weight"),
|
||||
sample_count=json_data.get("sample_count"),
|
||||
barcode=barcode,
|
||||
oss=object_name,
|
||||
net_weight_total=json_data.get("net_weight_total"),
|
||||
evaluator=json_data.get("evaluator"),
|
||||
reviewer=json_data.get("reviewer"),
|
||||
)
|
||||
|
||||
return json_data
|
||||
@@ -1,6 +1,27 @@
|
||||
import threading
|
||||
|
||||
|
||||
# 后台操作
|
||||
def async_db_task(func, *args, **kwargs):
|
||||
"""将数据库操作放到后台线程执行"""
|
||||
threading.Thread(target=func, args=args, kwargs=kwargs, daemon=True).start()
|
||||
threading.Thread(target=func, args=args, kwargs=kwargs, daemon=True).start()
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
async def async_task(func, *args, **kwargs):
|
||||
return await asyncio.to_thread(func, *args, **kwargs)
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
|
||||
|
||||
def format_datetime(dt: datetime, tz="Asia/Shanghai"):
|
||||
if dt.tzinfo is None:
|
||||
dt = pytz.UTC.localize(dt)
|
||||
tz_obj = pytz.timezone(tz)
|
||||
dt = dt.astimezone(tz_obj)
|
||||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
# 使用官方 Python 镜像
|
||||
FROM python:3.10-slim
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制依赖文件
|
||||
COPY requirements.txt .
|
||||
# 更新系统源,安装 PostgreSQL 和 ODBC 依赖,以及微软 SQL Server 驱动
|
||||
# 安装基础依赖和 Microsoft ODBC 驱动依赖
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
libpq5 \
|
||||
@@ -23,14 +18,25 @@ RUN apt-get update && \
|
||||
ACCEPT_EULA=Y apt-get install -y msodbcsql18 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
COPY docker/requirements.txt .
|
||||
# 安装 Python 依赖
|
||||
RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
|
||||
|
||||
# 复制并解压 JRE
|
||||
COPY docker/OpenJDK17U-jre_x64_linux_hotspot_17.0.16_8.tar.gz /opt/
|
||||
RUN tar -xzf /opt/OpenJDK17U-jre_x64_linux_hotspot_17.0.16_8.tar.gz -C /opt/ && \
|
||||
rm /opt/OpenJDK17U-jre_x64_linux_hotspot_17.0.16_8.tar.gz
|
||||
|
||||
# 配置 Java 环境
|
||||
ENV JAVA_HOME=/opt/jdk-17.0.16+8-jre
|
||||
ENV PATH="$JAVA_HOME/bin:$PATH"
|
||||
|
||||
|
||||
# 复制项目代码
|
||||
COPY app/ .
|
||||
|
||||
EXPOSE 13011
|
||||
|
||||
# 启动命令(使用 uvicorn 启动 FastAPI)
|
||||
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "13011"]
|
||||
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "13011", "--workers", "4"]
|
||||
|
||||
Binary file not shown.
@@ -16,3 +16,7 @@ typing_extensions==4.15.0
|
||||
uvicorn[standard]
|
||||
pyodbc==5.2.0
|
||||
dashscope==1.24.2
|
||||
minio==7.2.16
|
||||
pyzxing==1.1.1
|
||||
Pillow==11.3.0
|
||||
python-multipart==0.0.20
|
||||
@@ -0,0 +1,56 @@
|
||||
import os
|
||||
import base64
|
||||
from langchain.prompts import PromptTemplate
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langchain.output_parsers import JsonOutputParser
|
||||
from langchain.schema import HumanMessage
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# 定义你想要的结构化输出
|
||||
class ImageDescription(BaseModel):
|
||||
objects: list[str] = Field(description="图片里出现的主要物体")
|
||||
scene: str = Field(description="场景描述")
|
||||
mood: str = Field(description="整体氛围")
|
||||
|
||||
parser = JsonOutputParser(pydantic_object=ImageDescription)
|
||||
|
||||
prompt = PromptTemplate(
|
||||
template="""你是一个图像分析助手。请根据输入的图片内容和文字说明,
|
||||
输出符合下列 JSON schema 的结果:
|
||||
{
|
||||
"moisture_content": 12.5,
|
||||
"cocoon_weight": 15.2,
|
||||
"defective_pupa_count": 3,
|
||||
"fresh_shell_weight": 8.7,
|
||||
"sample_count": 50,
|
||||
"net_weight_total": 760,
|
||||
"evaluator": "张三",
|
||||
"reviewer": "李四",
|
||||
"barcode": "123456789012"
|
||||
}
|
||||
|
||||
输入内容:
|
||||
{query}
|
||||
""",
|
||||
input_variables=["query"],
|
||||
partial_variables={"format_instructions": parser.get_format_instructions()},
|
||||
)
|
||||
|
||||
def get_response(base64_image: str, system_text: str = "请分析这张图片并输出JSON结果"):
|
||||
messages = [
|
||||
HumanMessage(content=[
|
||||
{"type": "text", "text": system_text},
|
||||
# {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}} # 本地图片转成base64
|
||||
{"type": "image_url", "image_url": {"url": {base64_image}}} # 本地图片转成base64
|
||||
])
|
||||
]
|
||||
|
||||
# 串起来:prompt -> llm -> parser
|
||||
chain = prompt | llm | parser
|
||||
response = chain.invoke({"query": messages})
|
||||
print(response)
|
||||
|
||||
if __name__ == "__main__":
|
||||
with open("test.jpg", "rb") as f:
|
||||
base64_image = base64.b64encode(f.read()).decode("utf-8")
|
||||
get_response(base64_image)
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './iva';
|
||||
export * from './sca';
|
||||
export * from './ticket';
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { pyRequestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 获取已分析的图片列表
|
||||
*/
|
||||
export async function refreshTicketImageList() {
|
||||
return pyRequestClient.get('/llm/getTicketImageList');
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片分析任务
|
||||
*/
|
||||
export async function createTicketImageTask(formData: FormData) {
|
||||
return pyRequestClient.post('/llm/createTicketImageTask', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -8,7 +8,7 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: {
|
||||
icon: 'ic:round-remove-red-eye',
|
||||
authority: ['iva', 'sca', 'ysa'],
|
||||
keepAlive: false,
|
||||
keepAlive: true,
|
||||
order: 2,
|
||||
title: $t('计算机视觉'),
|
||||
},
|
||||
@@ -48,6 +48,17 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
component: () => import('#/views/cv/ysa/index.vue'),
|
||||
},
|
||||
{
|
||||
name: 'TICKET',
|
||||
path: '/cv/ticket',
|
||||
meta: {
|
||||
authority: ['ticket'],
|
||||
icon: 'mdi:ticket-confirmation',
|
||||
title: $t('仪评指标联分析'),
|
||||
keepAlive: true,
|
||||
},
|
||||
component: () => import('#/views/cv/ticket/index.vue'),
|
||||
},
|
||||
{
|
||||
name: 'CVAT',
|
||||
path: '/cv/cvat',
|
||||
|
||||
@@ -27,6 +27,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
onConfirm() {
|
||||
if (!projectName.value || !selectedFile.value) {
|
||||
message.warning('请填写项目名并选择视频文件');
|
||||
return; // 阻止继续执行
|
||||
}
|
||||
uploadFile();
|
||||
},
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Form, Input, message } from 'ant-design-vue';
|
||||
|
||||
import { createVideoTask } from '#/api/cv/iva';
|
||||
|
||||
const projectName = ref('');
|
||||
const year = ref<null | number>(null);
|
||||
const month = ref<null | number>(null);
|
||||
const day = ref<null | number>(null);
|
||||
const hours = ref<null | number>(null);
|
||||
const minutes = ref<null | number>(null);
|
||||
const seconds = ref<null | number>(null);
|
||||
const fileName = ref('');
|
||||
const selectedFile = ref<File | null>(null);
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||
@@ -27,6 +18,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
onConfirm() {
|
||||
if (!projectName.value || !selectedFile.value) {
|
||||
message.warning('请填写项目名并选择蚕茧图片');
|
||||
return;
|
||||
}
|
||||
uploadFile();
|
||||
},
|
||||
@@ -41,12 +33,6 @@ async function uploadFile() {
|
||||
modalApi.close();
|
||||
// 清空表单
|
||||
projectName.value = '';
|
||||
year.value = null;
|
||||
month.value = null;
|
||||
day.value = null;
|
||||
hours.value = null;
|
||||
minutes.value = null;
|
||||
seconds.value = null;
|
||||
fileName.value = '';
|
||||
selectedFile.value = null;
|
||||
});
|
||||
@@ -60,7 +46,7 @@ function handleFileChange(event: Event) {
|
||||
const files = (event.target as HTMLInputElement).files;
|
||||
if (files && files.length > 0) {
|
||||
selectedFile.value = files[0];
|
||||
fileName.value = files[0]!.name;
|
||||
fileName.value = files[0].name;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
<script setup lang="ts">
|
||||
import { onDeactivated, onMounted, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Form, Input, message } from 'ant-design-vue';
|
||||
|
||||
import * as api from '#/api';
|
||||
|
||||
const list = ref<any[]>([]);
|
||||
const error = ref<null | string>(null);
|
||||
const selectedItem = ref<any>(null);
|
||||
|
||||
async function loadList() {
|
||||
error.value = null;
|
||||
list.value = (await api.refreshTicketImageList()) || [];
|
||||
}
|
||||
|
||||
function createTask() {
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
async function selectItem(item: any) {
|
||||
selectedItem.value = item;
|
||||
refreshLineChart();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadList();
|
||||
});
|
||||
|
||||
const showInfoStr = ref<Record<string, number | string>>({});
|
||||
const showInfoStr2 = ref<Record<string, number | string>>({});
|
||||
|
||||
function refreshLineChart() {
|
||||
const data = selectedItem.value;
|
||||
showInfoStr.value = {
|
||||
项目名: data.name,
|
||||
上传时间: data.created_at,
|
||||
文件名: data.file_name,
|
||||
文件大小: `${data.size} MB`,
|
||||
分辨率: data.resolution,
|
||||
};
|
||||
showInfoStr2.value = {
|
||||
含水量: `${data.moisture_content} %`,
|
||||
下足茧重: `${data.cocoon_weight} g`,
|
||||
'非蛹/僵蚕': `${data.defective_pupa_count} 粒`,
|
||||
鲜壳量: `${data.fresh_shell_weight} g`,
|
||||
小样数: `${data.sample_count} 粒`,
|
||||
净重合计: `${data.net_weight_total} kg`,
|
||||
仪评人: data.evaluator,
|
||||
复核人: data.reviewer,
|
||||
条码: data.barcode,
|
||||
};
|
||||
}
|
||||
|
||||
onDeactivated(() => {
|
||||
// 离开路由时清理状态
|
||||
selectedItem.value = null;
|
||||
showInfoStr.value = {};
|
||||
});
|
||||
|
||||
const projectName = ref('');
|
||||
const fileName = ref('');
|
||||
const selectedFile = ref<File | null>(null);
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
title: '新建仪评指标联分析任务',
|
||||
class: 'w-[600px]',
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm() {
|
||||
if (!selectedFile.value) {
|
||||
message.warning('请选择指标联图片');
|
||||
return;
|
||||
}
|
||||
uploadFile();
|
||||
},
|
||||
});
|
||||
async function uploadFile() {
|
||||
if (!selectedFile.value) {
|
||||
message.warning('请选择图片');
|
||||
return;
|
||||
}
|
||||
|
||||
// 先关闭弹窗
|
||||
modalApi.close();
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedFile.value);
|
||||
formData.append('projectName', projectName.value);
|
||||
await api.createTicketImageTask(formData);
|
||||
|
||||
// 接口完成后再触发事件
|
||||
message.success('分析任务完成');
|
||||
loadList();
|
||||
|
||||
// 清空表单
|
||||
selectedFile.value = null;
|
||||
projectName.value = '';
|
||||
fileName.value = '';
|
||||
} catch {
|
||||
message.error('上传失败');
|
||||
}
|
||||
}
|
||||
|
||||
function selectFile() {
|
||||
fileInputRef.value?.click();
|
||||
}
|
||||
|
||||
function handleFileChange(event: Event) {
|
||||
const files = (event.target as HTMLInputElement).files;
|
||||
if (files && files.length > 0) {
|
||||
selectedFile.value = files[0];
|
||||
fileName.value = files[0].name;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-[90dvh] w-full flex-col">
|
||||
<Modal>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="任务名称">
|
||||
<Input v-model:value="projectName" placeholder="可为空,将取随机值" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="仪评指标联图片" required>
|
||||
<div
|
||||
@click="selectFile"
|
||||
style="
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border: 1px dashed #d9d9d9;
|
||||
"
|
||||
>
|
||||
{{ fileName || '点击选择文件' }}
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
ref="fileInputRef"
|
||||
@change="handleFileChange"
|
||||
style="display: none"
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<BaseModal />
|
||||
<div class="flex h-full w-full bg-gray-50">
|
||||
<!-- 左侧:筛选 + 列表 -->
|
||||
<div class="flex w-64 flex-col border-r bg-white p-4">
|
||||
<!-- 按钮组 -->
|
||||
<div class="mb-4 flex justify-between space-x-2">
|
||||
<Button type="primary" @click="createTask" class="flex-1">
|
||||
新建任务
|
||||
</Button>
|
||||
</div>
|
||||
<!-- 列表 -->
|
||||
<div class="flex-1 space-y-2 overflow-auto">
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item.v_id"
|
||||
@click="selectItem(item)"
|
||||
class="cursor-pointer rounded border p-3 hover:bg-gray-100"
|
||||
:class="{ 'bg-gray-100': item.id === selectedItem?.id }"
|
||||
>
|
||||
<div class="text-base font-medium">{{ item.name }}</div>
|
||||
<div class="text-sm text-gray-400">{{ item.created_at }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:Tab 内容区 -->
|
||||
<div class="flex flex-1 flex-col overflow-hidden p-6">
|
||||
<div
|
||||
v-if="!selectedItem"
|
||||
class="flex h-full items-center justify-center text-gray-400"
|
||||
>
|
||||
请先选择左侧列表中的分析任务
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="flex h-full flex-col gap-4">
|
||||
<!-- 主内容区域:左右结构 -->
|
||||
<div class="flex flex-1 gap-4">
|
||||
<!-- 左侧 -->
|
||||
<div class="flex w-72 flex-col gap-4">
|
||||
<!-- 视频基础信息展示 -->
|
||||
<div
|
||||
class="w-full rounded border bg-white p-4"
|
||||
id="video_base_info"
|
||||
>
|
||||
<div
|
||||
v-for="(value, key) in showInfoStr"
|
||||
:key="key"
|
||||
class="mb-2 flex text-sm text-gray-700"
|
||||
>
|
||||
<div class="w-28 font-medium text-gray-900">
|
||||
{{ key }}:
|
||||
</div>
|
||||
<div class="flex-1 break-all text-gray-600">
|
||||
{{ value || '—' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下:空白卡片 -->
|
||||
<div class="flex-1 rounded border bg-white p-4">
|
||||
<div
|
||||
v-for="(value, key) in showInfoStr2"
|
||||
:key="key"
|
||||
class="mb-2 flex text-sm text-gray-700"
|
||||
>
|
||||
<div class="w-32 font-medium text-gray-900">
|
||||
{{ key }}:
|
||||
</div>
|
||||
<div class="flex-1 break-all text-gray-600">
|
||||
{{ value || '—' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧 -->
|
||||
<div class="flex flex-1 flex-col gap-4">
|
||||
<!-- 上:左右两个图片显示 -->
|
||||
<!-- 左图 -->
|
||||
<div
|
||||
class="flex h-full w-full items-center justify-center rounded border bg-white p-4"
|
||||
>
|
||||
<img
|
||||
:src="selectedItem?.oss_url"
|
||||
alt="左图"
|
||||
class="h-[80dvh] w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,97 +2,105 @@
|
||||
import type {
|
||||
WorkbenchProjectItem,
|
||||
WorkbenchQuickNavItem,
|
||||
} from "@vben/common-ui";
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import { useRouter } from "vue-router";
|
||||
import { WorkbenchHeader, WorkbenchQuickNav } from "@vben/common-ui";
|
||||
import { preferences } from "@vben/preferences";
|
||||
import { useUserStore } from "@vben/stores";
|
||||
import { openWindow } from "@vben/utils";
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { WorkbenchHeader, WorkbenchQuickNav } from '@vben/common-ui';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
||||
const cv: WorkbenchQuickNavItem[] = [
|
||||
{
|
||||
color: "#3fb27f",
|
||||
color: '#3fb27f',
|
||||
authority: ['iva'],
|
||||
icon: "mdi:video",
|
||||
title: "视频智能分析",
|
||||
url: "/cv/iva"
|
||||
icon: 'mdi:video',
|
||||
title: '视频智能分析',
|
||||
url: '/cv/iva',
|
||||
},
|
||||
{
|
||||
color: "#3fb27f",
|
||||
color: '#3fb27f',
|
||||
authority: ['sca'],
|
||||
icon: "mdi:ice-cream",
|
||||
title: "蚕茧仪评分析",
|
||||
url: "/cv/sca"
|
||||
icon: 'mdi:ice-cream',
|
||||
title: '蚕茧仪评分析',
|
||||
url: '/cv/sca',
|
||||
},
|
||||
{
|
||||
color: "#3fb27f",
|
||||
color: '#3fb27f',
|
||||
authority: ['ysa'],
|
||||
icon: "mdi:waveform",
|
||||
title: "催青阶段分析",
|
||||
url: "/cv/ysa"
|
||||
icon: 'mdi:waveform',
|
||||
title: '催青阶段分析',
|
||||
url: '/cv/ysa',
|
||||
},
|
||||
{
|
||||
color: "#3fb27f",
|
||||
icon: "ion:bar-chart-outline",
|
||||
title: "标注平台入口",
|
||||
color: '#3fb27f',
|
||||
authority: ['ticket'],
|
||||
icon: 'mdi:ticket-confirmation',
|
||||
title: '仪评指标联分析',
|
||||
url: '/cv/ticket',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
icon: 'ion:bar-chart-outline',
|
||||
title: '标注平台入口',
|
||||
authority: ['user'],
|
||||
url: "http://171.212.101.199:13013/"
|
||||
url: 'http://171.212.101.199:13013/',
|
||||
},
|
||||
];
|
||||
const llm: WorkbenchQuickNavItem[] = [
|
||||
{
|
||||
color: "#1fdaca",
|
||||
color: '#1fdaca',
|
||||
authority: ['bot'],
|
||||
icon: "mdi:face-agent",
|
||||
title: "通用智能体",
|
||||
url: "/llm/bot"
|
||||
icon: 'mdi:face-agent',
|
||||
title: '通用智能体',
|
||||
url: '/llm/bot',
|
||||
},
|
||||
{
|
||||
color: "#1fdaca",
|
||||
color: '#1fdaca',
|
||||
authority: ['report'],
|
||||
icon: 'mdi:set-center',
|
||||
title: "智农观数阁",
|
||||
url: "/llm/report/report-chat"
|
||||
title: '智农观数阁',
|
||||
url: '/llm/report/report-chat',
|
||||
},
|
||||
{
|
||||
color: "#1fdaca",
|
||||
color: '#1fdaca',
|
||||
authority: ['service'],
|
||||
icon: 'mdi:android-head',
|
||||
title: "灵思智服阁",
|
||||
url: "/llm/service/service-chat"
|
||||
title: '灵思智服阁',
|
||||
url: '/llm/service/service-chat',
|
||||
},
|
||||
];
|
||||
const common: WorkbenchQuickNavItem[] = [
|
||||
{
|
||||
color: "#bf0c2c",
|
||||
color: '#bf0c2c',
|
||||
authority: ['remote'],
|
||||
icon: "carbon:workspace",
|
||||
title: "设备远程控制",
|
||||
url: "/remote"
|
||||
icon: 'carbon:workspace',
|
||||
title: '设备远程控制',
|
||||
url: '/remote',
|
||||
},
|
||||
{
|
||||
color: "#bf0c2c",
|
||||
color: '#bf0c2c',
|
||||
icon: 'ion:grid-outline',
|
||||
title: "RAGFlow",
|
||||
title: 'RAGFlow',
|
||||
authority: ['user'],
|
||||
url: "/out/rag"
|
||||
url: '/out/rag',
|
||||
},
|
||||
];
|
||||
const router = useRouter();
|
||||
|
||||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
|
||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
if (nav.url?.startsWith("http")) {
|
||||
if (nav.url?.startsWith('http')) {
|
||||
openWindow(nav.url);
|
||||
return;
|
||||
}
|
||||
if (nav.url?.startsWith("/")) {
|
||||
if (nav.url?.startsWith('/')) {
|
||||
router.push(nav.url).catch((error) => {
|
||||
console.error("Navigation failed:", error);
|
||||
console.error('Navigation failed:', error);
|
||||
});
|
||||
} else {
|
||||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
|
||||
@@ -101,10 +109,10 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
|
||||
function getGreeting() {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 6) return "凌晨好";
|
||||
if (hour < 12) return "早安";
|
||||
if (hour < 18) return "下午好";
|
||||
return "晚上好";
|
||||
if (hour < 6) return '凌晨好';
|
||||
if (hour < 12) return '早安';
|
||||
if (hour < 18) return '下午好';
|
||||
return '晚上好';
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user