仪评指标联分析模块
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user