Bladeren bron

feat: stash temp

licanglong 2 maanden geleden
bovenliggende
commit
223c1b11ab

+ 0 - 3
app/App.py

@@ -13,9 +13,6 @@ from app.handler import ApplicationStartupEvent
 _log = logging.getLogger(__name__)
 
 
-
-
-
 class App(ABC):
     """
     app 模块

+ 0 - 8
app/blueprints/base/__init__.py

@@ -1,8 +0,0 @@
-# @description: 
-# @author: licanglong
-# @date: 2025/11/20 14:21
-from flask import Blueprint
-
-base_bp = Blueprint("base", __name__, url_prefix='/')
-
-from app.blueprints.base import routes  # noqa

+ 0 - 8
app/blueprints/invoice/__init__.py

@@ -1,8 +0,0 @@
-# @description: 
-# @author: licanglong
-# @date: 2025/11/20 14:21
-from flask import Blueprint
-
-invoice_bp = Blueprint("invoice", __name__, url_prefix='/invoice')
-
-from app.blueprints.invoice import routes  # noqa

+ 0 - 36
app/blueprints/invoice/routes.py

@@ -1,36 +0,0 @@
-# @description: 
-# @author: licanglong
-# @date: 2025/11/20 14:22
-
-from flask import request, jsonify
-
-from app.blueprints.invoice import invoice_bp
-from app.db.dbsession import SessionLocal
-from app.models.Result import SysResult
-from app.repositories.invoice_repo import InvoiceRepository
-
-
-@invoice_bp.route('/list/by_tax_id', methods=['POST'])
-def list_by_taxid():
-    data = request.json or {}
-    session = SessionLocal()
-    if not data or not data.get('tax_id', None):
-        return jsonify(SysResult.fail(msg="参数错误"))
-    try:
-        repo = InvoiceRepository(session)
-        invoices = repo.list_by_taxid(data['tax_id'])
-        return jsonify(SysResult.success(data=invoices.dict()['invoice_purchase_details']))
-    finally:
-        session.close()
-
-
-@invoice_bp.route('/list/by_params', methods=['POST'])
-def list_by_params():
-    data = request.json or {}
-    session = SessionLocal()
-    try:
-        repo = InvoiceRepository(session)
-        invoices = repo.list_by_params(data.get("tax_id", None), data.get("hwmc", None))
-        return jsonify(SysResult.success(data=invoices.dict()['invoice_purchase_details']))
-    finally:
-        session.close()

+ 0 - 8
app/blueprints/risk/__init__.py

@@ -1,8 +0,0 @@
-# @description: 
-# @author: licanglong
-# @date: 2025/11/20 14:21
-from flask import Blueprint
-
-risk_bp = Blueprint("risk", __name__, url_prefix='/risk')
-
-from app.blueprints.risk import routes  # noqa

+ 0 - 119
app/blueprints/risk/routes.py

@@ -1,119 +0,0 @@
-# @description: 
-# @author: licanglong
-# @date: 2025/11/20 14:22
-import json
-
-import openai
-from flask import jsonify, request
-from openai.types.chat import ChatCompletion
-
-from app.blueprints.risk import risk_bp
-from app.client.VectorStoreClient import vector_store_client
-from app.constants.vector_store import VectorStoreCollection
-from app.core import BizException, CTX
-from app.models.Result import SysResult
-from app.models.dto import FinalDecisionResult, RiskEvidenceResult
-from app.prompt import person_consumption_prompt, external_evidence_search_prompt
-from app.utils import AIStreamJSONParser
-
-
-@risk_bp.route('/decide', methods=['POST'])
-def risk_decide():
-    """
-    发票风险裁决
-    :return:
-    """
-    invoice_data = request.json
-    vector = vector_store_client.embedding.encode(f"""
-        特定业务类型:{invoice_data['tdywlx'] or ''} 
-        购买方名称: {invoice_data['gmfmc'] or ''} 
-        货物名称:{invoice_data['hwmc'] or ''} 
-        规格型号:{invoice_data['ggxh'] or ''} 
-        开票人:{invoice_data['kpr'] or ''}
-        """)
-    rules = vector_store_client.client.query_points(
-        collection_name=VectorStoreCollection.RULE_EMBED_STORE,
-        query=vector.tolist(),
-        limit=5,
-        score_threshold=0.5
-    )
-    cases = vector_store_client.client.query_points(
-        collection_name=VectorStoreCollection.CASE_EMBED_STORE,
-        query=vector.tolist(),
-        limit=5,
-        score_threshold=0.5
-    )
-
-    merchants = vector_store_client.client.query_points(
-        collection_name=VectorStoreCollection.MERCHANTS_EMBED_STORE,
-        query=vector.tolist(),
-        limit=5,
-        score_threshold=0.5
-    )
-
-    edges = vector_store_client.client.query_points(
-        collection_name=VectorStoreCollection.EDGES_EMBED_STORE,
-        query=vector.tolist(),
-        limit=5,
-        score_threshold=0.5
-    )
-    input_data = {
-        "invoice_context": invoice_data,
-        "rules": [hit.payload for hit in rules.points],
-        "cases": [hit.payload for hit in cases.points],
-        "industry": [hit.payload for hit in merchants.points],
-        "signals": [hit.payload for hit in edges.points]
-    }
-
-    final_user_prompt = person_consumption_prompt.get_person_consumption_user_prompt(
-        json.dumps(input_data, ensure_ascii=False))
-
-    final_user_prompt = final_user_prompt.replace("{{input_data_desc}}", "")
-    client = openai.OpenAI(
-        api_key=CTX.ENV.getprop("llm.qwen.api_key", raise_error=True),
-        base_url=CTX.ENV.getprop("llm.qwen.base_url", raise_error=True),
-    )
-    completion = client.chat.completions.create(
-        model="qwen-plus",
-        messages=[{'role': 'system', 'content': person_consumption_prompt.system_prompt},
-                  {'role': 'user', 'content': final_user_prompt}],
-        stream=True,
-        stream_options={"include_usage": True}
-    )
-    parser = AIStreamJSONParser()
-    for chunk in completion:
-        parser.feed_chunk(chunk.model_dump_json())
-        if parser.is_finished(chunk.model_dump_json()):
-            break
-    result = parser.get_result()
-    decision_result: FinalDecisionResult = FinalDecisionResult.model_validate(result)
-    return jsonify(SysResult.success(data=decision_result.dict()))
-
-
-@risk_bp.route('/evidence', methods=['POST'])
-def evidence_replenish():
-    invoice_data = request.json
-
-    input_data = {
-        "invoice_context": invoice_data
-    }
-    final_external_evidence_user_prompt = external_evidence_search_prompt.get_external_evidence_user_prompt(
-        json.dumps(input_data, ensure_ascii=False))
-    client = openai.OpenAI(
-        api_key=CTX.ENV.getprop("llm.qwen.api_key", raise_error=True),
-        base_url=CTX.ENV.getprop("llm.qwen.base_url", raise_error=True),
-    )
-    completion: ChatCompletion = client.chat.completions.create(
-        model="qwen-plus",
-        messages=[
-            {'role': 'system', 'content': external_evidence_search_prompt.external_evidence_system_prompt},
-            {'role': 'user', 'content': final_external_evidence_user_prompt}],
-        extra_body={"enable_search": True,
-                    "search_options": {"search_strategy": "agent", "enable_source": True}}
-
-    )
-    if not completion.choices:
-        raise BizException("LLM响应异常")
-    generate_content = completion.choices[0].message.content
-    evidence_result: RiskEvidenceResult = RiskEvidenceResult.model_validate(json.loads(generate_content))
-    return jsonify(SysResult.success(data=evidence_result.dict()))

+ 0 - 8
app/blueprints/vector_store/__init__.py

@@ -1,8 +0,0 @@
-# @description: 
-# @author: licanglong
-# @date: 2025/11/20 14:21
-from flask import Blueprint
-
-vector_store_bp = Blueprint("vector_store", __name__, url_prefix='/vector_store')
-
-from app.blueprints.vector_store import routes  # noqa

+ 28 - 0
app/call_tools/llm_tools.py

@@ -0,0 +1,28 @@
+# @description: 
+# @author: licanglong
+# @date: 2025/12/24 14:33
+from typing import Callable
+
+from pydantic import BaseModel, Field
+
+
+class LLMTool(BaseModel):
+    pass
+
+
+class LLMDynamicTool(LLMTool):
+    name: str
+    description: str
+    parameters: dict = {}
+    handler: Callable[..., ...]
+
+
+class AliSearchWebTool(LLMTool):
+    """
+    Search the web for information.
+    """
+    name: str
+    description: str
+    parameters: dict = {"query": ""}
+    handler: Callable[..., ...]
+    query: str = Field(description="The query to search the web for.当你需要从互联网上搜索相关信息时调用")

+ 5 - 5
app/client/VectorStoreClient.py

@@ -3,7 +3,7 @@
 # @date: 2025/12/19 16:08
 import logging
 
-from qdrant_client import QdrantClient
+from qdrant_client import AsyncQdrantClient
 from sentence_transformers import SentenceTransformer
 
 from app.core import CTX
@@ -24,15 +24,15 @@ class VectorStoreClient:
         if not model_path:
             raise ValueError("model_path cannot be empty")
         # 1. Qdrant 客户端
-        self.client = QdrantClient(host=host, port=port)
+        self.client = AsyncQdrantClient(host=host, port=port)
         # 2. 向量模型
         self.embedding = SentenceTransformer(model_path)
 
-    def create_collection(self, collection_name: str, **kwargs) -> bool:
-        if self.client.collection_exists(collection_name):
+    async def create_collection(self, collection_name: str, **kwargs) -> bool:
+        if await self.client.collection_exists(collection_name):
             return False
 
-        self.client.create_collection(
+        await self.client.create_collection(
             collection_name=collection_name,
             **kwargs,
         )

+ 11 - 15
app/models/Result.py

@@ -1,34 +1,30 @@
 # @description: 
 # @author: licanglong
 # @date: 2025/11/20 14:49
-from dataclasses import dataclass
+from typing import Optional
+
+from pydantic import BaseModel
 
 from app.core import SYS_SERVER_FAIL, SYS_SERVER_SUCCESS
 from app.utils.typeutils import T
 
 
-@dataclass
-class SysResult:
-    code: int
-    msg: str
-    data: T
-
-    def __init__(self, code=None, msg=None, data=None):
-        self.code = code
-        self.msg = msg
-        self.data = data
+class SysResult(BaseModel):
+    code: Optional[int] = None
+    msg: Optional[str] = None
+    data: Optional[T] = None
 
     @staticmethod
-    def fail(code=None, msg=None, data=None):
+    def fail(code: Optional[int] = None, msg: Optional[str] = None, data: Optional[T] = None):
         if code is None:
             code = SYS_SERVER_FAIL
         return SysResult(code=code, msg=msg, data=data)
 
     @staticmethod
-    def success(code=None, msg=None, data=None):
+    def success(code: Optional[int] = None, msg: Optional[str] = None, data: Optional[T] = None):
         if code is None:
             code = SYS_SERVER_SUCCESS
         return SysResult(code=code, msg=msg, data=data)
 
-    def __str__(self):
-        return f"code:{self.code},msg:{self.msg},data:{self.data}"
+    # def __str__(self):
+    #     return f"code:{self.code},msg:{self.msg},data:{self.data}"

+ 33 - 0
app/models/dto/risk_dto.py

@@ -134,3 +134,36 @@ class RiskEvidenceResult(BaseModel):
     cases: List[RiskDecisionCase]
     industry: List[MerchantIndustryProfile]
     signals: List[RiskSignal]
+
+
+"""
+{{
+            "info":"<string:商品信息>",
+            "type":"<string:商品分类>",
+            "decision": "<BELONG | NOT_BELONG | UNCERTAIN>",
+            "confidence":<float:置信度(0.0~1.0)>,
+            "summary":"<string:最终判断结论,需要明确当前判断的数据所属类型,并且给出依据>",
+            "evidence_chain":<list:[
+                  {{
+                    "summary": "<string:该证据对最终判断产生的关键影响>",
+                    "confidence":<float:置信度(0.0~1.0)>,
+                    "source": "<引用来源>"
+                  }}
+                ]>
+            }}
+"""
+
+
+class SimilarIdentificationEvidence(BaseModel):
+    summary: str = Field(..., description="该证据对最终判断产生的关键影响")
+    confidence: float = Field(..., description="置信度,范围是 0.0 到 1.0")
+    source: str = Field(..., description="引用来源")
+
+
+class SimilarIdentificationResult(BaseModel):
+    info: str = Field(..., description="商品信息")
+    type: str = Field(..., description="商品分类")
+    decision: str = Field(..., description="判断结果:BELONG | NOT_BELONG | UNCERTAIN")
+    confidence: float = Field(..., description="置信度,范围是 0.0 到 1.0")
+    summary: str = Field(..., description="最终判断结论,需要明确当前判断的数据所属类型,并且给出依据")
+    evidence_chain: List[SimilarIdentificationEvidence] = Field(..., description="证据链,包含一系列对判断有影响的证据")

+ 4 - 0
app/models/params/__init__.py

@@ -0,0 +1,4 @@
+# @description: 
+# @author: licanglong
+# @date: 2026/1/4 11:27
+from app.models.params.invoice_params import *

+ 15 - 0
app/models/params/invoice_params.py

@@ -0,0 +1,15 @@
+# @description: 
+# @author: licanglong
+# @date: 2026/1/4 11:27
+from typing import Optional
+
+from pydantic import BaseModel, Field
+
+
+class ListInvoiceParams(BaseModel):
+    tax_id: Optional[str] = Field(default=None, description="税号")
+    hwmc: Optional[str] = Field(default=None, description="货物名称")
+
+
+class ListInvoiceByTaxId(BaseModel):
+    tax_id: Optional[str] = Field(default=None, description="税号")

+ 165 - 0
app/prompt/industry_data_extractor_prompt.py

@@ -0,0 +1,165 @@
+# @description: 
+# @author: licanglong
+# @date: 2026/1/5 13:58
+system_prompt = """
+你是一名「数据结构化抽取专家」。
+你的唯一职责是:**从用户提供的原始资料中,准确抽取信息,并严格按照用户指定的结构输出结构化数据**。
+
+你不负责分析资料价值,不负责补充背景,不负责推理缺失信息,也不负责给出任何解释性文字。
+
+────────────────
+【核心任务定义(必须严格遵守)】
+
+你的任务包括且仅包括以下内容:
+1. 阅读并理解用户提供的原始资料内容
+2. 识别资料中与目标结构字段一一对应的可推理出信息
+3. 将可抽取的信息填充到用户指定的结构中
+4. 当资料中不存在、无法明确对应或存在歧义的信息时,将对应字段置为“空值”
+
+────────────────
+【必须严格遵守的规则】
+
+- 所有输出字段必须**完全符合**用户给定的结构定义(字段名、层级、类型)
+- 输出结构中**不允许新增、删除或重命名任何字段**
+- 每一个字段的值必须**直接来源于用户提供的资料**
+- 当字段在资料中无法找到明确对应内容时,必须置空
+- 输出内容中不允许出现任何解释性、说明性或评价性文本
+
+────────────────
+【严格禁止的行为(必须遵守)】
+
+- ❌ 不得编造、补充或假设任何资料内容
+- ❌ 不得使用常识、背景知识或外部信息填充字段
+- ❌ 不得调整、简化或概括原始资料表述
+- ❌ 不得输出结构化数据以外的任何文本(包括说明、注释、提示)
+- ❌ 不得出现“同上”“未知”“推测”“可能是”等非结构化描述
+
+────────────────
+【标准执行步骤(不得跳过、合并或省略)】
+
+步骤一:结构理解  
+- 完整理解用户给出的目标结构,包括字段名称、层级关系与数据类型  
+
+步骤二:资料对齐  
+- 逐字段在用户提供的资料中查找是否存在可直接映射的内容  
+
+步骤三:字段抽取  
+- 若存在明确、唯一对应的资料内容,则按原意抽取并填入字段  
+- 若不存在或存在歧义,则将该字段置为空值  
+
+步骤四:结构校验  
+- 校验最终输出是否:
+  - 完全符合目标结构
+  - 字段数量、层级、顺序一致
+  - 不包含任何多余字段或文本  
+
+────────────────
+【空值规则(必须遵守)】
+
+- 空值必须使用用户指定的空值形式  
+- 若用户未指定空值形式,则统一使用:null
+- 不得使用占位文本(如“无”“不详”“待补充”)
+
+────────────────
+【输出要求(最高优先级)】
+
+- 输出内容 **只能** 是符合要求的结构化数据
+- 禁止在输出前后添加任何说明性文字
+- 禁止使用 Markdown、代码块或自然语言包装输出结果
+"""
+
+
+def user_prompt(industry_data):
+    return f"""
+    ======================
+    # 用户需求分析资料
+    ======================
+    {industry_data[4]}
+    
+    ======================
+    # 行业信息
+    ======================
+    国民经济行业分类代码(中类):{industry_data[0]}
+    国民经济行业分类名称(中类):{industry_data[1]}
+    税收编码:{industry_data[2]}
+    税收编码简称:{industry_data[3]}
+    
+    ======================
+    # 行业相关资料
+    ======================
+    {industry_data[5]}
+    
+    ======================
+    # 【输出 JSON Schema(必须严格遵守)】严格输出此JSON结构,不得随意篡改,必须严格遵守
+    ======================
+    {{
+  "industry_basic": {{
+    "nec_code_mid": "<string:国民经济行业分类代码(中类)>",
+    "nec_name_mid": "<string:国民经济行业分类名称(中类)>",
+    "tax_code": "<string:税收编码>",
+    "tax_code_abbr": "<string:税收编码简称>"
+  }},
+  "cost_structure": {{
+    "material_ratio": {{
+      "value": "<string:材料占比>",
+      "source": "<string:数据来源>"
+    }},
+    "labor_ratio": {{
+      "value": "<string:人工占比>",
+      "source": "<string:数据来源>"
+    }},
+    "expense_ratio": {{
+      "value": "<string:费用占比>",
+      "source": "<string:数据来源>"
+    }}
+  }},
+  "profit_indicators": {{
+    "gross_margin": {{
+      "value": "<string:毛利率(如:22% - 30%)>",
+      "source": "<string:数据来源>"
+    }},
+    "net_margin": {{
+      "value": "<string:净利率(如:22% - 30%)>",
+      "source": "<string:数据来源>"
+    }}
+  }},
+  "expense_sales_ratios": {{
+    "sales_exp_ratio": {{
+      "value": "<string:销售费用占营收比(如:22% - 30%)>",
+      "source": "<string:数据来源>"
+    }},
+    "admin_exp_ratio": {{
+      "value": "<string:管理费用占营收比(如:22% - 30%)>",
+      "source": "<string:数据来源>"
+    }},
+    "finance_exp_ratio": {{
+      "value": "<string:财务费用占营收比(如:22% - 30%)>",
+      "source": "<string:数据来源>"
+    }}
+  }},
+  "operational_indicators": {{
+    "labor_efficiency_ratio": {{
+      "value": "<string:工业企业工效比(如:22% - 30%)>",
+      "source": "<string:数据来源>"
+    }},
+    "asset_turnover": {{
+      "value": "<string:资产周转率(如:22% - 30%)>",
+      "source": "<string:数据来源>"
+    }}
+  }},
+  "product_material": {{
+    "main_products": [
+      {{
+        "product_name": "<string:一种产品名称>",
+        "source": "<string:数据来源>",
+        "material_composition": [
+          {{
+            "material_name": "<string:该产品材料名称>",
+            "ratio": "<string:该材料占比(如 5%-8%)>"
+          }}
+        ]
+      }}
+    ]
+  }}
+}}
+    """

+ 165 - 0
app/prompt/industry_data_search.py

@@ -0,0 +1,165 @@
+# @description: 
+# @author: licanglong
+# @date: 2026/1/6 16:16
+system_prompt = """
+你是一名「数据结构化抽取专家」。
+你的唯一职责是:**从用户提供的原始资料中,准确抽取信息,并严格按照用户指定的结构输出结构化数据**。
+
+你不负责分析资料价值,不负责补充背景,不负责推理缺失信息,也不负责给出任何解释性文字。
+
+────────────────
+【核心任务定义(必须严格遵守)】
+
+你的任务包括且仅包括以下内容:
+1. 阅读并理解用户提供的原始资料内容
+2. 识别资料中与目标结构字段一一对应的可推理出信息
+3. 将可抽取的信息填充到用户指定的结构中
+4. 当资料中不存在、无法明确对应或存在歧义的信息时,将对应字段置为“空值”
+
+────────────────
+【必须严格遵守的规则】
+
+- 所有输出字段必须**完全符合**用户给定的结构定义(字段名、层级、类型)
+- 输出结构中**不允许新增、删除或重命名任何字段**
+- 每一个字段的值必须**直接来源于用户提供的资料**
+- 当字段在资料中无法找到明确对应内容时,必须置空
+- 输出内容中不允许出现任何解释性、说明性或评价性文本
+
+────────────────
+【严格禁止的行为(必须遵守)】
+
+- ❌ 不得编造、补充或假设任何资料内容
+- ❌ 不得使用常识、背景知识或外部信息填充字段
+- ❌ 不得调整、简化或概括原始资料表述
+- ❌ 不得输出结构化数据以外的任何文本(包括说明、注释、提示)
+- ❌ 不得出现“同上”“未知”“推测”“可能是”等非结构化描述
+
+────────────────
+【标准执行步骤(不得跳过、合并或省略)】
+
+步骤一:结构理解  
+- 完整理解用户给出的目标结构,包括字段名称、层级关系与数据类型  
+
+步骤二:资料对齐  
+- 逐字段在用户提供的资料中查找是否存在可直接映射的内容  
+
+步骤三:字段抽取  
+- 若存在明确、唯一对应的资料内容,则按原意抽取并填入字段  
+- 若不存在或存在歧义,则将该字段置为空值  
+
+步骤四:结构校验  
+- 校验最终输出是否:
+  - 完全符合目标结构
+  - 字段数量、层级、顺序一致
+  - 不包含任何多余字段或文本  
+
+────────────────
+【空值规则(必须遵守)】
+
+- 空值必须使用用户指定的空值形式  
+- 若用户未指定空值形式,则统一使用:null
+- 不得使用占位文本(如“无”“不详”“待补充”)
+
+────────────────
+【输出要求(最高优先级)】
+
+- 输出内容 **只能** 是符合要求的结构化数据
+- 禁止在输出前后添加任何说明性文字
+- 禁止使用 Markdown、代码块或自然语言包装输出结果
+"""
+
+
+def user_prompt(industry_data):
+    return f"""
+    ======================
+    # 用户需求分析资料
+    ======================
+    {industry_data[4]}
+
+    ======================
+    # 行业信息
+    ======================
+    国民经济行业分类代码(中类):{industry_data[0]}
+    国民经济行业分类名称(中类):{industry_data[1]}
+    税收编码:{industry_data[2]}
+    税收编码简称:{industry_data[3]}
+
+    ======================
+    # 行业相关资料
+    ======================
+    {industry_data[5]}
+
+    ======================
+    # 【输出 JSON Schema(必须严格遵守)】严格输出此JSON结构,不得随意篡改,必须严格遵守
+    ======================
+    {{
+  "industry_basic": {{
+    "nec_code_mid": "<string:国民经济行业分类代码(中类)>",
+    "nec_name_mid": "<string:国民经济行业分类名称(中类)>",
+    "tax_code": "<string:税收编码>",
+    "tax_code_abbr": "<string:税收编码简称>"
+  }},
+  "cost_structure": {{
+    "material_ratio": {{
+      "value": "<string:材料占比>",
+      "source": "<string:数据来源>"
+    }},
+    "labor_ratio": {{
+      "value": "<string:人工占比>",
+      "source": "<string:数据来源>"
+    }},
+    "expense_ratio": {{
+      "value": "<string:费用占比>",
+      "source": "<string:数据来源>"
+    }}
+  }},
+  "profit_indicators": {{
+    "gross_margin": {{
+      "value": "<string:毛利率(如:22% - 30%)>",
+      "source": "<string:数据来源>"
+    }},
+    "net_margin": {{
+      "value": "<string:净利率(如:22% - 30%)>",
+      "source": "<string:数据来源>"
+    }}
+  }},
+  "expense_sales_ratios": {{
+    "sales_exp_ratio": {{
+      "value": "<string:销售费用占营收比(如:22% - 30%)>",
+      "source": "<string:数据来源>"
+    }},
+    "admin_exp_ratio": {{
+      "value": "<string:管理费用占营收比(如:22% - 30%)>",
+      "source": "<string:数据来源>"
+    }},
+    "finance_exp_ratio": {{
+      "value": "<string:财务费用占营收比(如:22% - 30%)>",
+      "source": "<string:数据来源>"
+    }}
+  }},
+  "operational_indicators": {{
+    "labor_efficiency_ratio": {{
+      "value": "<string:工业企业工效比(如:22% - 30%)>",
+      "source": "<string:数据来源>"
+    }},
+    "asset_turnover": {{
+      "value": "<string:资产周转率(如:22% - 30%)>",
+      "source": "<string:数据来源>"
+    }}
+  }},
+  "product_material": {{
+    "main_products": [
+      {{
+        "product_name": "<string:一种产品名称>",
+        "source": "<string:数据来源>",
+        "material_composition": [
+          {{
+            "material_name": "<string:该产品材料名称>",
+            "ratio": "<string:该材料占比(如 5%-8%)>"
+          }}
+        ]
+      }}
+    ]
+  }}
+}}
+    """

+ 77 - 0
app/prompt/industry_planner_prompt.py

@@ -0,0 +1,77 @@
+# @description: 
+# @author: licanglong
+# @date: 2026/1/5 11:35
+system_prompt = """
+你是一名「高级需求分析与规划专家」,你的唯一职责是:**理解需求、拆解需求、分析需求**,而不是设计解决方案或实现方案。
+
+在分析过程中,如果你发现自己对用户需求中的概念、背景、行业语境或专业术语理解不充分,你**可以通过联网搜索来补充认知**,但该搜索行为仅用于“理解需求本身”,**严禁用于直接解决需求或给出方案结论**。
+
+
+
+【分析流程】
+
+步骤一:需求理解概览 (允许联网)
+- 对用户给出的原始需求进行详细、完整的理解性分析  
+- 若需求存在模糊点、不确定点或隐含背景,请明确指出  
+- 此步骤的目标是“说明你是如何理解这个需求的”,而不是评价或解决  
+
+步骤二:需求拆解与分点分析  
+- 基于步骤一的理解结果,将整体需求拆解为多个明确的分析分点  
+- 每个分点必须有清晰的分析对象,例如:
+  - 业务目标
+  - 使用场景
+  - 规则或约束
+  - 数据或信息依赖
+  - 技术或系统边界
+  - 风险与不确定性
+- 分点之间应逻辑独立,但共同构成完整需求全貌  
+
+步骤三:需求理解所需的信息判断
+- 针对步骤二中的每一个分点,判断你是否**已经具备充分认知**来理解该分点  
+- 如果用户提出的需求需要额外的数据支撑,你可以规划如何搜索互联网,给出搜索方案和搜索关键词。
+- 不得通过搜索来推导解决方案或给出结论  
+
+你必须严格遵循以下分析流程,任何步骤不得跳过、合并或简化。
+
+【核心约束(必须严格遵守)】
+- 你的输出是“需求分析”,不是“解决方案”
+- 不允许使用“同上”“略”“省略”“如前所述”等任何简化表达,需要给出完整未缩略的需求分析和需求支撑方案
+- 每一个分析分点都必须是完整、独立、自洽的描述
+- 即使多个分点存在相似背景,也必须分别完整展开说明
+- 所有分析应尽可能详细、明确、结构化
+
+【固定输出格式(必须严格遵守)】
+
+1. <分点标题>
+分点详细分析:
+- 对该分点涉及的背景、目标、边界、假设条件、不确定性进行完整说明
+- 说明该分点在整体需求中的作用与重要性
+
+此需求是否需要外部信息:
+- 是否需要联网搜索:是 / 否
+- 若需要联网搜索,其目的:
+  - …
+- 联网搜索方案:
+ - ...
+- 搜索关键词:
+  - 关键词1
+  - 关键词2
+"""
+
+
+# 料工费比例,毛利率,费销比率(销售费用/营业收入,管理费用/营业收入,财务费用/营业收入),工业企业工效比,资产周转率,净利率,行业主要产品及其材料构成
+def user_prompt(input_data) -> str:
+    return f"""
+    我需要从权威平台获取资料并整理某个行业最新发布的(需要获取当前时间)的料工费比例。
+    推荐权威平台:
+        - 政府 / 监管机构
+        - 行业协会 / 行业白皮书
+        - 权威百科或标准定义
+        - 大型平台的公开说明
+    信息时效要求:最新,最好是当年或近几年的
+    下面我给出行业信息:
+    国民经济行业分类代码(中类):{input_data[0]}
+    国民经济行业分类名称(中类):{input_data[1]}
+    税收编码:{input_data[2]}
+    税收编码简称:{input_data[3]}
+    """

+ 1 - 1
app/prompt/person_consumption_prompt.py

@@ -90,7 +90,7 @@ def get_person_consumption_user_prompt(input_data: str, input_data_structure: Op
             
             # 【输出 JSON Schema(必须严格遵守)】
             {{
-              "decision": "<PERSONAL_CONSUMPTION | ENTERPRISE_OPERATION | ENTERPRISE_WELFARE | UNCERTAIN",
+              "decision": "<PERSONAL_CONSUMPTION | ENTERPRISE_OPERATION | ENTERPRISE_WELFARE | UNCERTAIN>",
               "confidence": "<float:置信度(0.0~1.0)>",
               "completion": {{
                 "summary": "<string:最终判断结论,需要明确当前判断的数据所属类型,并且给出依据>",

+ 74 - 0
app/prompt/similar_identification_prompt.py

@@ -0,0 +1,74 @@
+# @description: 
+# @author: licanglong
+# @date: 2025/12/24 14:45
+similar_identification_system_prompt = """
+你是一个发票内容同类识别引擎,而不是通用对话模型。
+
+你的任务是:根据给出的商品名称,判断其是否属于指定的分类。并给出充分的判断依据。你可以参考公开的分类标准来做出判断,并给出相应的置信度。
+
+你必须严格遵守以下规则:
+【强制约束】
+1. 如果存在以下任一情况:
+   - 证据不足
+   - 证据之间存在明显冲突
+   - 描述模糊难以判断,误判率较高时
+   你必须输出:UNCERTAIN
+
+2. 你必须启用联网搜索能力
+
+3. 你只能使用以下类型的信息来源:
+   - 政府 / 监管机构
+   - 行业协会 / 行业白皮书
+   - 权威百科或标准定义
+   - 大型平台的公开说明
+4. 严禁:
+   - 使用主观推断
+   - 使用个人经验或常识
+   - 编造事实或来源
+5. 你必须严格按照【输出 JSON Schema】返回结果:
+   - 不得输出 Markdown
+   - 不得输出多余字段
+   - 不得输出任何没有基于输入证据的解释性文字
+   - 允许在 evidence_chain.summary 中进行“证据到结论的结构化说明”,当没有 任何引用时,evidence_chain应该为空
+   - summary 面对用户总结,需要使用业务语言而不是系统技术语言
+        - 面向普通用户
+        - 只能使用业务语言解释“为什么这么判断”
+
+"""
+
+
+def get_similar_identification_user_prompt(data: str, type: str):
+    return f"""
+            # 输入的数据
+            商品信息:{data}
+            商品分类:{type}
+            
+            # 【判断步骤(必须逐步执行,不得跳过)】
+            请严格按以下步骤进行判断:
+            
+            步骤一:商品信息和商品分类进行初步分析,得出它们之间得关联和差异
+            步骤二:针对商品信息和商品分类还有步骤以一得出的关联和差异,进行联网搜索,获取相关信息
+            步骤三:对联网得到的信息进行校验,判断其置信度和有效性,对这些信息进行清洗得到安全数据,如果信息来源是链接,必须严格保证链接的可用性
+            步骤四:结合步骤一和步骤三得到的所有信息进行总结判断,给出最终结论
+            
+            # 【结论要求(必须严格遵守)】
+            - 最终判断结论应该详细具体,保证可读性
+            - 如果来源是链接,必须严格保证链接的可用性,并且保证source和summary的相关性
+            - 如果来源是内容应该简洁易懂
+            
+            # 【输出 JSON Schema(必须严格遵守)】
+            {{
+            "info":"<string:商品信息>",
+            "type":"<string:商品分类>",
+            "decision": "<BELONG | NOT_BELONG | UNCERTAIN>",
+            "confidence":<float:置信度(0.0~1.0)>,
+            "summary":"<string:最终判断结论,需要明确当前判断的数据所属类型,并且给出依据>",
+            "evidence_chain":<list:[
+                  {{
+                    "summary": "<string:该证据对最终判断产生的关键影响>",
+                    "confidence":<float:置信度(0.0~1.0)>,
+                    "source": "<引用来源,如果来源是链接,必须严格保证链接的可用性,并且保证source和summary的相关性,如果是内容应该简洁>"
+                  }}
+                ]>
+            }}
+           """

+ 88 - 0
app/prompt/source_researcher_prompt.py

@@ -0,0 +1,88 @@
+# @description: 
+# @author: licanglong
+# @date: 2026/1/5 11:39
+system_prompt = """
+你是一名「数据收集与整理专家」,具备联网搜索能力。
+你的唯一任务是:**为用户的既定需求与方案提供充分、可靠、可支撑的数据材料,并进行结构化整理**。
+
+你不是需求分析专家,也不是方案设计或决策模型。
+你不负责提出新方案、不负责判断方案优劣、不负责给出结论性建议。
+
+────────────────
+【核心任务定义(必须严格遵守)】
+
+你的任务包括且仅包括以下三点:
+1. 基于用户给出的需求与方案,识别需要被数据支撑的具体需求点  
+2. 有选择性地进行联网数据收集,以补足这些需求点所需的信息  
+3. 当用户需求有时效性要求时,进行联网数据收集时必须要带上时间范围进行搜索限制  
+4. 当数据量与信息完整度“足以支撑需求理解或后续决策”时,对数据进行分类、整理与结构化输出
+
+────────────────
+【标准执行步骤(不得跳步)】
+
+步骤一:需求与方案理解  
+- 基于用户输入,明确当前需要被数据支撑的目标与范围  
+- 列出“哪些需求点需要数据支撑”,而不是“如何解决需求”  
+
+步骤二:数据需求拆解  
+- 将整体数据需求拆解为多个明确的数据收集分点  
+- 每个分点应说明:
+  - 对应的需求点
+  - 需要哪一类数据(定义、规模、规则、事实、案例、统计等)
+
+步骤三:联网数据收集  
+- 针对每一个数据分点进行有针对性的联网搜索  
+- 搜索目标是获取**客观信息与事实数据**,而非结论  
+- 避免过度搜索,与需求无关的数据不应纳入  
+
+步骤四:数据充分性判断  
+- 判断当前已收集的数据是否已经:
+  - 能够支撑需求理解
+  - 能够为后续分析或决策提供基础材料  
+- 若数据不足,应继续补充;若已足够,进入下一步  
+
+步骤五:分类整理与结构化输出  
+- 按需求点对数据进行分类整理  
+- 确保每一类数据内部逻辑清晰、信息完整、表述具体  
+- 输出应以“数据说明”为主,而非分析结论
+
+────────────────
+【必须严格遵守的原则】
+- 所有数据收集行为必须“围绕用户已给出的需求与方案”
+- 只收集**支撑性数据、事实信息、背景资料、客观描述**
+- 数据来源应尽量可靠、明确(如官方资料、权威机构、公开文档)
+- 数据整理必须与需求点一一对应,避免无关信息堆砌
+- 输出内容应完整、清晰、可直接被下游模块或人工使用
+- 最终输出内容为列举的资料,无需输出整理和分析过程,只输出整理好的资料,严禁输出不符和要求的结构
+
+────────────────
+【严格禁止的行为(必须遵守)】
+
+- ❌ 不得修改、扩展或重新定义用户的需求
+- ❌ 不得提出新的解决方案、策略或实施建议
+- ❌ 不得对方案进行评价、对比、优劣判断
+- ❌ 不得用主观推断代替数据事实
+- ❌ 不得出现“同上”“略”“省略”“概述即可”等简化表达
+- ❌ 不得在数据不足的情况下强行给出总结性结论
+
+────────────────
+【最终输出结构(必须严格遵守,不得输出额外内容和推理分析的过程)】
+
+1. <需求点 / 数据分点标题>
+对应需求说明:
+- 该数据分点支撑的具体需求是什么
+收集到的关键数据:
+- 数据点 1(事实 / 定义 / 规则 / 描述)
+- 数据点 2
+- 数据点 3
+
+2. <需求点 / 数据分点标题>
+...
+"""
+
+
+def user_prompt(input_data: str):
+    return f"""
+    # 用户需求
+    {input_data}
+    """

+ 4 - 3
app/repositories/invoice_repo.py

@@ -13,7 +13,7 @@ class InvoiceRepository:
     def __init__(self, session: Session):
         self.session = session
 
-    def list_by_taxid(self, tax_id: str) -> InvoicePurchaseDetailList:
+    async def list_by_taxid(self, tax_id: str) -> InvoicePurchaseDetailList:
         stmt = select(InvoicePurchaseDetail).where(InvoicePurchaseDetail.tax_id == tax_id).limit(10)
         stmt_result = self.session.scalars(stmt).all()
         return InvoicePurchaseDetailList(
@@ -23,7 +23,8 @@ class InvoiceRepository:
             ]
         )
 
-    def list_by_params(self, tax_id: str = None, hwmc: str = None) -> InvoicePurchaseDetailList:
+    async def list_by_params(self, tax_id: str = None, hwmc: str = None, page_number: int = 1,
+                             page_size: int = 10) -> InvoicePurchaseDetailList:
         conditions = []
         if tax_id:
             conditions.append(InvoicePurchaseDetail.tax_id == tax_id)
@@ -34,7 +35,7 @@ class InvoiceRepository:
 
         if conditions:
             stmt = stmt.where(*conditions)
-        stmt = stmt.limit(10)
+        stmt = stmt.offset((page_number - 1) * page_size).limit(page_size)
         stmt_result = self.session.scalars(stmt).all()
         return InvoicePurchaseDetailList(
             invoice_purchase_details=[

+ 0 - 0
app/blueprints/__init__.py → app/routes/__init__.py


+ 8 - 0
app/routes/base/__init__.py

@@ -0,0 +1,8 @@
+# @description: 
+# @author: licanglong
+# @date: 2025/11/20 14:21
+from fastapi import APIRouter
+
+# base_bp = Blueprint("base", __name__, url_prefix='/')
+base_router = APIRouter(tags=['base'])
+from app.routes.base import routes  # noqa

+ 81 - 0
app/routes/base/routes.py

@@ -0,0 +1,81 @@
+# @description: 
+# @author: licanglong
+# @date: 2025/11/20 14:22
+import dirtyjson
+
+from app.prompt import industry_planner_prompt, source_researcher_prompt, industry_data_extractor_prompt
+from app.routes.base import base_router
+from app.service.llm_client import llm_call
+
+
+@base_router.post("/industry/data")
+async def get_industry_data():
+    industry_input_data = ["011", "谷物种植", "1010101000000000000", "谷物"]
+    tools = [
+        {
+            "type": "function",
+            "function": {
+                "name": "get_current_time",
+                "description": "用于获取当前时间,当需求中涉及到了时间概念时可以使用",
+            }
+        }
+        ,
+        {
+            "type": "function",
+            "function": {
+                "name": "ali_search_tool",
+                "description": "当需要从互联网获取额外信息支撑时使用",
+                "parameters": {
+                    "type": "object",
+                    "properties": {
+                        "keyword": {
+                            "type": "string",
+                            "description": "用于搜索的完整关键词,尽量将问题描述完整。如需限定来源,可在结尾使用“+ 来源机构名称”。"
+                        }
+                    },
+                    "required": ["keyword"]
+                }
+            }
+        }
+
+    ]
+
+    industry_planner_messages = [
+        {
+            "role": "system",
+            "content": industry_planner_prompt.system_prompt
+        },
+        {
+            "role": "user",
+            "content": industry_planner_prompt.user_prompt(industry_input_data)
+        }
+    ]
+
+    industry_planner_data = await llm_call(tools=tools, messages=industry_planner_messages)
+
+    source_researcher_messages = [
+        {
+            "role": "system",
+            "content": source_researcher_prompt.system_prompt
+        },
+        {
+            "role": "user",
+            "content": source_researcher_prompt.user_prompt(industry_planner_data)
+        }
+    ]
+    source_researcher_data = await llm_call(tools=tools, messages=source_researcher_messages)
+
+    industry_data_extractor_messages = [
+        {
+            "role": "system",
+            "content": industry_data_extractor_prompt.system_prompt
+        },
+        {
+            "role": "user",
+            "content": industry_data_extractor_prompt.user_prompt(
+                [*industry_input_data, industry_planner_data, source_researcher_data])
+        }
+    ]
+
+    industry_data_extractor = await llm_call(tools=tools, messages=industry_data_extractor_messages)
+    return dirtyjson.loads(industry_data_extractor)

+ 7 - 0
app/routes/invoice/__init__.py

@@ -0,0 +1,7 @@
+# @description: 
+# @author: licanglong
+# @date: 2025/11/20 14:21
+from fastapi import APIRouter
+
+invoice_router = APIRouter(tags=['invoice'], prefix='/invoice')
+from app.routes.invoice import routes  # noqa

+ 41 - 0
app/routes/invoice/routes.py

@@ -0,0 +1,41 @@
+# @description: 
+# @author: licanglong
+# @date: 2025/11/20 14:22
+
+from flask import request
+
+from app.routes.invoice import invoice_router
+from app.db.dbsession import SessionLocal
+from app.models.Result import SysResult
+from app.models.params import ListInvoiceParams, ListInvoiceByTaxId
+from app.repositories.invoice_repo import InvoiceRepository
+
+
+@invoice_router.post('/list/by_tax_id')
+async def list_by_taxid(params: ListInvoiceByTaxId):
+    data = request.json or {}
+    session = SessionLocal()
+    if not params or not params.tax_id:
+        return SysResult.fail(msg="参数错误")
+    try:
+        repo = InvoiceRepository(session)
+        invoices = await repo.list_by_taxid(params.tax_id)
+        return SysResult.success(data=invoices.invoice_purchase_details)
+    finally:
+        session.close()
+
+
+@invoice_router.post('/list/by_params')
+async def list_by_params(params: ListInvoiceParams, pageNumber: int = 1, pageSize: int = 10):
+    if not pageNumber or pageNumber == 0:
+        pageNumber = 1
+    if not pageSize or pageSize == 0:
+        pageSize = 10
+    session = SessionLocal()
+    try:
+        repo = InvoiceRepository(session)
+        invoices = await repo.list_by_params(params.tax_id, params.hwmc, int(pageNumber),
+                                             int(pageSize))
+        return SysResult.success(data=invoices.invoice_purchase_details)
+    finally:
+        session.close()

+ 7 - 0
app/routes/risk/__init__.py

@@ -0,0 +1,7 @@
+# @description: 
+# @author: licanglong
+# @date: 2025/11/20 14:21
+from fastapi import APIRouter
+
+risk_router = APIRouter(tags=['risk'], prefix='/risk')
+from app.routes.risk import routes  # noqa

+ 166 - 0
app/routes/risk/routes.py

@@ -0,0 +1,166 @@
+# @description: 
+# @author: licanglong
+# @date: 2025/11/20 14:22
+import json
+import re
+
+import dirtyjson
+import openai
+
+from app.client.VectorStoreClient import vector_store_client
+from app.constants.vector_store import VectorStoreCollection
+from app.core import BizException, CTX
+from app.models.Result import SysResult
+from app.models.dto import FinalDecisionResult, RiskEvidenceResult, SimilarIdentificationResult
+from app.prompt import person_consumption_prompt, external_evidence_search_prompt, similar_identification_prompt
+from app.routes.risk import risk_router
+from app.service.llm_client import llm_call
+
+
+@risk_router.post('/decide')
+async def risk_decide(invoice_data: dict):
+    """
+    发票风险裁决
+    :return:
+    """
+    vector = vector_store_client.embedding.encode(f"""
+        特定业务类型:{invoice_data['tdywlx'] or ''} 
+        购买方名称: {invoice_data['gmfmc'] or ''} 
+        货物名称:{invoice_data['hwmc'] or ''} 
+        规格型号:{invoice_data['ggxh'] or ''} 
+        开票人:{invoice_data['kpr'] or ''}
+        """)
+    rules = await vector_store_client.client.query_points(
+        collection_name=VectorStoreCollection.RULE_EMBED_STORE,
+        query=vector.tolist(),
+        limit=5,
+        score_threshold=0.5
+    )
+    cases = await vector_store_client.client.query_points(
+        collection_name=VectorStoreCollection.CASE_EMBED_STORE,
+        query=vector.tolist(),
+        limit=5,
+        score_threshold=0.5
+    )
+
+    merchants = await vector_store_client.client.query_points(
+        collection_name=VectorStoreCollection.MERCHANTS_EMBED_STORE,
+        query=vector.tolist(),
+        limit=5,
+        score_threshold=0.5
+    )
+
+    edges = await vector_store_client.client.query_points(
+        collection_name=VectorStoreCollection.EDGES_EMBED_STORE,
+        query=vector.tolist(),
+        limit=5,
+        score_threshold=0.5
+    )
+    input_data = {
+        "invoice_context": invoice_data,
+        "rules": [hit.payload for hit in rules.points],
+        "cases": [hit.payload for hit in cases.points],
+        "industry": [hit.payload for hit in merchants.points],
+        "signals": [hit.payload for hit in edges.points]
+    }
+
+    final_user_prompt = person_consumption_prompt.get_person_consumption_user_prompt(
+        json.dumps(input_data, ensure_ascii=False))
+
+    final_user_prompt = final_user_prompt.replace("{{input_data_desc}}", "")
+    client = openai.AsyncOpenAI(
+        api_key=CTX.ENV.getprop("llm.qwen.api_key", raise_error=True),
+        base_url=CTX.ENV.getprop("llm.qwen.base_url", raise_error=True),
+    )
+    completion = await client.chat.completions.create(
+        model="qwen-plus",
+        messages=[{'role': 'system', 'content': person_consumption_prompt.system_prompt},
+                  {'role': 'user', 'content': final_user_prompt}]
+    )
+    if not completion.choices:
+        raise BizException("LLM响应异常")
+    generate_content = completion.choices[0].message.content
+    decision_result: FinalDecisionResult = FinalDecisionResult.model_validate(dirtyjson.loads(generate_content))
+    return SysResult.success(data=decision_result)
+
+
+@risk_router.post('/evidence')
+async def evidence_replenish(invoice_data: dict):
+    input_data = {
+        "invoice_context": invoice_data
+    }
+    final_external_evidence_user_prompt = external_evidence_search_prompt.get_external_evidence_user_prompt(
+        json.dumps(input_data, ensure_ascii=False))
+    tools = [
+        {
+            "type": "function",
+            "function": {
+                "name": "ali_search_tool",
+                "description": "当需要从互联网获取额外信息时使用",
+                "parameters": {
+                    "type": "object",
+                    "properties": {
+                        "keyword": {
+                            "type": "string",
+                            "description": "搜索关键词,如果需要限定搜索源可以在结尾加上 <+ 平台名称 >,例如: 如何判断一个企业的经营范围? + 税务局"
+                        }
+                    },
+                    "required": ["keyword"]
+                }
+            }
+        }
+    ]
+
+    generate_content = await llm_call(tools=tools, messages=[
+        {'role': 'system', 'content': external_evidence_search_prompt.external_evidence_system_prompt},
+        {'role': 'user', 'content': final_external_evidence_user_prompt}])
+    evidence_result: RiskEvidenceResult = RiskEvidenceResult.model_validate(dirtyjson.loads(generate_content))
+    return SysResult.success(data=evidence_result)
+
+
+@risk_router.post('/similar')
+async def similar_identification(invoice_data: dict):
+    hwmc = invoice_data["hwmc"]
+    hw_type = None
+    type_match = re.match(r'\*([^*]+)\*', hwmc)
+    if type_match:
+        hw_type = type_match[0]
+
+    info = re.sub(r'\*([^*]+)\*', "", hwmc)
+    if not hw_type or not info:
+        return SysResult.fail(msg="货物信息不符合规范")
+
+    system_prompt = similar_identification_prompt.similar_identification_system_prompt
+    user_prompt = similar_identification_prompt.get_similar_identification_user_prompt(info, hw_type)
+
+    client = openai.AsyncOpenAI(
+        api_key=CTX.ENV.getprop("llm.qwen.api_key", raise_error=True),
+        base_url=CTX.ENV.getprop("llm.qwen.base_url", raise_error=True),
+    )
+
+    tools = [
+        {
+            "type": "function",
+            "function": {
+                "name": "ali_search_tool",
+                "description": "当需要从互联网获取额外信息时使用",
+                "parameters": {
+                    "type": "object",
+                    "properties": {
+                        "keyword": {
+                            "type": "string",
+                            "description": "搜索关键词,如果需要限定搜索源可以在结尾加上 <+ 平台名称 >,例如: 行业指标 + 税务局"
+                        }
+                    },
+                    "required": ["keyword"]
+                }
+            }
+        }
+    ]
+
+    generate_content = await llm_call(tools=tools, messages=[
+        {'role': 'system', 'content': system_prompt},
+        {'role': 'user', 'content': user_prompt}])
+    identification_result: SimilarIdentificationResult = SimilarIdentificationResult.model_validate(
+        dirtyjson.loads(generate_content))
+    return SysResult.success(data=identification_result)

+ 7 - 0
app/routes/vector_store/__init__.py

@@ -0,0 +1,7 @@
+# @description: 
+# @author: licanglong
+# @date: 2025/11/20 14:21
+from fastapi import APIRouter
+
+vector_store_router = APIRouter(tags=['vector_store'], prefix='/vector_store')
+from app.routes.vector_store import routes  # noqa

+ 25 - 31
app/blueprints/vector_store/routes.py → app/routes/vector_store/routes.py

@@ -4,22 +4,20 @@
 import uuid
 from typing import List
 
-from flask import request, jsonify
 from qdrant_client.models import VectorParams, Distance, PointStruct
 
-from app.blueprints.vector_store import vector_store_bp
+from app.routes.vector_store import vector_store_router
 from app.client.VectorStoreClient import vector_store_client
 from app.constants.vector_store import VectorStoreCollection
 from app.models.Result import SysResult
 from app.models.dto import RiskRuleList, RiskDecisionCaseList, IndustryProfileList, RiskSignalList
 
 
-@vector_store_bp.route('/risk/rule', methods=['PUT'])
-def put_risk_rule():
-    data = request.json or {}
+@vector_store_router.put('/risk/rule')
+async def put_risk_rule(data: dict):
     risk_rules = RiskRuleList(risk_rules=data).risk_rules
     collection_name = VectorStoreCollection.RULE_EMBED_STORE
-    vector_store_client.create_collection(
+    await vector_store_client.create_collection(
         collection_name=collection_name,
         vectors_config=VectorParams(
             size=1792,
@@ -41,19 +39,18 @@ def put_risk_rule():
             )
         )
 
-    vector_store_client.client.upsert(
+    await vector_store_client.client.upsert(
         collection_name=collection_name,
         points=points,
     )
-    return jsonify(SysResult.success())
+    return SysResult.success()
 
 
-@vector_store_bp.route('/risk/case', methods=['PUT'])
-def put_case_rule():
-    data = request.json or {}
+@vector_store_router.put('/risk/case')
+async def put_case_rule(data: dict):
     decision_cases = RiskDecisionCaseList(decision_cases=data).decision_cases
     collection_name = VectorStoreCollection.CASE_EMBED_STORE
-    vector_store_client.create_collection(
+    await vector_store_client.create_collection(
         collection_name=collection_name,
         vectors_config=VectorParams(
             size=1792,
@@ -74,19 +71,18 @@ def put_case_rule():
                 payload=item.dict(),
             )
         )
-    vector_store_client.client.upsert(
+    await vector_store_client.client.upsert(
         collection_name=collection_name,
         points=points,
     )
-    return jsonify(SysResult.success())
+    return SysResult.success()
 
 
-@vector_store_bp.route('/risk/industry', methods=['PUT'])
-def put_industry_rule():
-    data = request.json or {}
+@vector_store_router.put('/risk/industry')
+async def put_industry_rule(data: dict):
     industry_profiles = IndustryProfileList(industry_profiles=data).industry_profiles
     collection_name = VectorStoreCollection.MERCHANTS_EMBED_STORE
-    vector_store_client.create_collection(
+    await vector_store_client.create_collection(
         collection_name=collection_name,
         vectors_config=VectorParams(
             size=1792,
@@ -108,19 +104,18 @@ def put_industry_rule():
             )
         )
 
-    vector_store_client.client.upsert(
+    await vector_store_client.client.upsert(
         collection_name=collection_name,
         points=points,
     )
-    return jsonify(SysResult.success())
+    return SysResult.success()
 
 
-@vector_store_bp.route('/risk/signal', methods=['PUT'])
-def put_signal_rule():
-    data = request.json or {}
+@vector_store_router.put('/risk/signal')
+async def put_signal_rule(data: dict):
     signals = RiskSignalList(signals=data).signals
     collection_name = VectorStoreCollection.RULE_EMBED_STORE
-    vector_store_client.create_collection(
+    await vector_store_client.create_collection(
         collection_name=collection_name,
         vectors_config=VectorParams(
             size=1792,
@@ -142,18 +137,17 @@ def put_signal_rule():
             )
         )
 
-    vector_store_client.client.upsert(
+    await vector_store_client.client.upsert(
         collection_name=collection_name,
         points=points,
     )
-    return jsonify(SysResult.success())
+    return SysResult.success()
 
 
-@vector_store_bp.route('/risk/rule', methods=['GET'])
-def get_risk_rule():
-    data = request.json or {}
+@vector_store_router.get('/risk/rule')
+async def get_risk_rule(data: dict):
     vector = vector_store_client.embedding.encode(data.get('query', ""))
-    query_response = vector_store_client.client.query_points(
+    query_response = await vector_store_client.client.query_points(
         collection_name=VectorStoreCollection.RULE_EMBED_STORE,
         query=vector.tolist(),
         limit=5,
@@ -166,4 +160,4 @@ def get_risk_rule():
             "score": point.score,
             "payload": point.payload,
         })
-    return jsonify(SysResult.success(data=rules))
+    return SysResult.success(data=rules)

+ 1 - 1
app/blueprints/base/routes.py → app/service/__init__.py

@@ -1,3 +1,3 @@
 # @description: 
 # @author: licanglong
-# @date: 2025/11/20 14:22
+# @date: 2026/1/5 9:47

+ 64 - 0
app/service/llm_client.py

@@ -0,0 +1,64 @@
+# @description: 
+# @author: licanglong
+# @date: 2026/1/5 9:47
+import json
+
+import openai
+from openai.types.chat import ChatCompletion
+
+from app.core import CTX, BizException
+from app.utils import ali_search_tool, get_current_time
+
+
+class LLMClient:
+
+    def get_llm_provider(self):
+        pass
+
+    def generate(self) -> str:
+        pass
+
+
+async def llm_call(tools, messages):
+    client = openai.AsyncOpenAI(
+        api_key=CTX.ENV.getprop("llm.qwen.api_key", raise_error=True),
+        base_url=CTX.ENV.getprop("llm.qwen.base_url", raise_error=True),
+    )
+
+    completion: ChatCompletion = await client.chat.completions.create(
+        model="qwen-plus-latest",
+        messages=messages,
+        tools=tools if tools else None,
+        tool_choice="auto" if tools else "none",
+        # extra_body={
+        #     "enable_thinking": True
+        # }
+    )
+    if not completion.choices:
+        raise BizException("LLM响应异常")
+
+    if completion.choices[0].finish_reason == "tool_calls":
+        tool_infos = []
+        tool_calls = completion.choices[0].message.tool_calls
+        for call in tool_calls:
+            if call.function.name == "ali_search_tool":
+                args = json.loads(call.function.arguments)
+                keyword = args["keyword"]
+                info = await ali_search_tool(keyword)
+                info['pageItems'] = info['pageItems'][:(min(5, len(info['pageItems'])))]
+                tool_infos.append({
+                    "role": "tool",
+                    "tool_call_id": call.id,
+                    "content": json.dumps(info, ensure_ascii=False)
+                })
+            elif call.function.name == "get_current_time":
+                info = get_current_time()
+                tool_infos.append({
+                    "role": "tool",
+                    "tool_call_id": call.id,
+                    "content": info
+                })
+        return await llm_call(tools, [*messages, completion.choices[0].message, *tool_infos])
+
+    generate_content = completion.choices[0].message.content
+    return generate_content

+ 5 - 0
app/service/llm_provider.py

@@ -0,0 +1,5 @@
+# @description: 
+# @author: licanglong
+# @date: 2026/1/5 10:44
+class LLMProvider:
+    pass

+ 30 - 0
app/utils/__init__.py

@@ -1,11 +1,16 @@
 # @description: 
 # @author: licanglong
 # @date: 2025/9/24 14:07
+import datetime
 import hashlib
 import json
 import re
 from typing import Dict, Any
 
+from aiohttp import ClientSession
+
+from app.core import BizException
+
 
 class AIStreamJSONParser:
     """
@@ -77,3 +82,28 @@ def compute_text_hash(text: str) -> str:
     """
     normalized = normalize_text(text)
     return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
+
+
+def get_current_time():
+    return f"The current date and time is {datetime.datetime.now()}"
+
+
+async def ali_search_tool(text: str):
+    from app.core import CTX
+    async with ClientSession() as session:
+        search_resp = await session.post(url="https://cloud-iqs.aliyuncs.com/search/unified",
+                                         headers={"Content-Type": "application/json",
+                                                  "Authorization": f"Bearer {CTX.ENV.getprop('aliyun.search.api_key')}"},
+                                         json={
+                                             "query": text,
+                                             "engineType": "Generic",
+                                             "contents": {
+                                                 "mainText": True,
+                                                 "markdownText": False,
+                                                 "summary": False,
+                                                 "rerankScore": True
+                                             }
+                                         })
+        if search_resp.status != 200:
+            raise BizException("搜索工具调用异常")
+        return await search_resp.json(encoding="utf-8")

+ 4 - 1
env/env.yml

@@ -4,4 +4,7 @@ llm:
     base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
 qdrant:
   host: 117.72.147.109
-  port: 16333
+  port: 16333
+aliyun:
+  search:
+    api_key: VQwlJeOFbfVREpzlgqwMcYfunh8jx802OTVhMzE0OA

+ 26 - 0
pyproject.toml

@@ -0,0 +1,26 @@
+[project]
+name = "project"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.11"
+dependencies = [
+    "beautifulsoup4==4.14.3",
+    "colorama==0.4.6",
+    "concurrent-log==1.0.1",
+    "Flask==3.1.2",
+    "flask-cors==6.0.2",
+    "google-genai==1.56.0",
+    "numpy==2.3.5",
+    "openai==2.14.0",
+    "pandas==2.3.3",
+    "pillow==12.0.0",
+    "pydantic==2.12.5",
+    "PyMySQL==1.1.2",
+    "PyYAML==6.0.3",
+    "qdrant-client==1.16.1",
+    "requests==2.32.5",
+    "SQLAlchemy==2.0.45",
+    "transformers==4.57.3",
+    "waitress==3.0.2",
+]

+ 58 - 66
run.py

@@ -2,95 +2,87 @@
 # @author: licanglong
 # @date: 2025/11/20 11:41
 import logging
-import multiprocessing
 import os
-import sys
 
+import uvicorn
+from fastapi import FastAPI
 from pydantic import ValidationError
+from starlette.requests import Request
+from starlette.responses import JSONResponse
 
 # 设置环境变量
 os.environ["APP_PATH"] = os.getenv('APP_PATH') or os.path.abspath(os.path.dirname(__file__))  # noqa
 
 from app.App import App
 from app.core import BizException, CTX
-from app.models.Result import SysResult
 
 CTX.DEFAULT_LOG_FILE = "logs/app.log"  # noqa
-from flask import Flask, jsonify, request
-from flask_cors import CORS
 
 
 class AppImpl(App):
 
     def create_app(self):
-        from app.blueprints.base import base_bp
-        from app.blueprints.risk import risk_bp
-        from app.blueprints.vector_store import vector_store_bp
-        from app.blueprints.invoice import invoice_bp
-        app = Flask(__name__)
-
-        # 初始化扩展
-        CORS(app, supports_credentials=True)
-        app.config['JSON_AS_ASCII'] = False
-
-        # 注册 Blueprint
-        app.register_blueprint(base_bp)
-        app.register_blueprint(risk_bp)
-        app.register_blueprint(vector_store_bp)
-        app.register_blueprint(invoice_bp)
-
-        @app.before_request
-        def before():
-            pass
-
-        @app.after_request
-        def after_request(response):
-            """为每个请求添加 CORS 头"""
-            return response
-
-        @app.errorhandler(404)
-        def handle_404_error(e):
-            # 打印请求路径
-            logging.error(f"error request url: {request.path}")
-            logging.exception(e)
-            return jsonify(SysResult.fail(code=404, msg=str(e)))
+        from app.models.Result import SysResult
+        from app.routes.base import base_router
+        from app.routes.risk import risk_router
+        from app.routes.vector_store import vector_store_router
+        from app.routes.invoice import invoice_router
 
-        @app.errorhandler(BizException)
-        def biz_error_handle(e):
-            logging.exception(e.message)
-            return jsonify(SysResult.fail(msg=e.message or "服务异常", code=e.code))
+        fast_app = FastAPI()
 
-        @app.errorhandler(ValidationError)
-        def biz_error_handle(e):
+        @fast_app.exception_handler(404)
+        async def handle_404_error(request: Request, exc: Exception):
+            # 打印请求路径
+            logging.error(f"error request url [404]: {request.url}")
+            logging.exception(exc)
+            result = SysResult.fail(code=404, msg=str(exc))
+            return JSONResponse(
+                status_code=404,
+                content=result.model_dump()
+            )
+
+        @fast_app.exception_handler(BizException)
+        async def biz_error_handle(request: Request, exc: BizException):
+            logging.exception(exc.message)
+            result = SysResult.fail(msg=exc.message or "服务异常", code=exc.code)
+            return JSONResponse(
+                status_code=500,
+                content=result.model_dump()
+            )
+
+        @fast_app.exception_handler(ValidationError)
+        async def biz_error_handle(request: Request, exc: Exception):
             logging.exception("数据类型不匹配")
-            return jsonify(SysResult.fail(msg="数据类型不匹配", code=5001))
-
-        @app.errorhandler(Exception)
-        def exception_handle(e):
-            logging.exception(e)
-            return jsonify(SysResult.fail(msg="服务异常"))
-
-        return app
+            return JSONResponse(
+                status_code=500,
+                content=SysResult.fail(msg="数据类型不匹配", code=5001).model_dump()
+            )
+
+        @fast_app.exception_handler(Exception)
+        async def exception_handle(request: Request, exc: Exception):
+            logging.exception("服务异常")
+            return JSONResponse(
+                status_code=500,
+                content=SysResult.fail(msg="服务异常").model_dump()
+            )
+
+        # 注册路由
+        fast_app.include_router(base_router)
+        fast_app.include_router(invoice_router)
+        fast_app.include_router(risk_router)
+        fast_app.include_router(vector_store_router)
+
+        return fast_app
 
     def run(self, *args, **kwargs):
-        _app = self.create_app()
+        app = self.create_app()
         port = CTX.ENV.getprop("server.port", 7699)
         try:
-            if getattr(sys, 'frozen', False):
-                from waitress import serve
-
-                if sys.platform == "win32":
-                    import msvcrt
-
-                    multiprocessing.freeze_support()
-                    serve(_app, host='0.0.0.0', port=port)
-                    msvcrt.getch()
-                else:
-                    serve(_app, host='0.0.0.0', port=port)
-
-            else:
-                _app.run(host="0.0.0.0", port=port)
-
+            uvicorn.run(
+                app=app,
+                host="0.0.0.0",
+                port=port,
+            )
         except KeyboardInterrupt:
             logging.warning("程序终止!!")